From b7b3bb815dd0fd7fd8138a46fab49f9129a81fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 2 Jul 2018 12:39:40 +0200 Subject: [PATCH 001/145] start feature branch From 717f121a3effe0c04990c9c5cbcbb004d946980e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 2 Jul 2018 13:50:28 +0200 Subject: [PATCH 002/145] update .hgignore --- .hgignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.hgignore b/.hgignore index 6ff185f670..f9b5452045 100644 --- a/.hgignore +++ b/.hgignore @@ -29,3 +29,8 @@ Desktop DF$ # jrebel rebel.xml \.pyc +# ui +scm-ui/node-modules/ +scm-ui/yarn.lock +scm-ui/.gitignore +scm-ui/package-lock.json From 61eeadcd84d1dceeb95d281abcb653cac0ac1fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 2 Jul 2018 13:51:24 +0200 Subject: [PATCH 003/145] update .hgignore --- .hgignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hgignore b/.hgignore index f9b5452045..71fbaa8735 100644 --- a/.hgignore +++ b/.hgignore @@ -30,7 +30,7 @@ Desktop DF$ rebel.xml \.pyc # ui -scm-ui/node-modules/ +scm-ui/node_modules/ scm-ui/yarn.lock scm-ui/.gitignore scm-ui/package-lock.json From cdbdbf40cab05135e10a9bbb3e0753a5831b8e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 2 Jul 2018 14:50:13 +0200 Subject: [PATCH 004/145] initial import for scm ui --- .hgignore | 3 +- scm-ui/package.json | 27 +++++++ scm-ui/public/favicon.ico | Bin 0 -> 3870 bytes scm-ui/public/index.html | 40 ++++++++++ scm-ui/public/manifest.json | 15 ++++ scm-ui/src/App.js | 20 +++++ scm-ui/src/Main.js | 36 +++++++++ scm-ui/src/Navigation.js | 52 +++++++++++++ scm-ui/src/containers/Page.js | 3 + scm-ui/src/createReduxStore.js | 22 ++++++ scm-ui/src/index.js | 32 ++++++++ scm-ui/src/modules/page.js | 3 + scm-ui/src/registerServiceWorker.js | 117 ++++++++++++++++++++++++++++ scm.iml | 16 ++++ 14 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 scm-ui/package.json create mode 100644 scm-ui/public/favicon.ico create mode 100644 scm-ui/public/index.html create mode 100644 scm-ui/public/manifest.json create mode 100644 scm-ui/src/App.js create mode 100644 scm-ui/src/Main.js create mode 100644 scm-ui/src/Navigation.js create mode 100644 scm-ui/src/containers/Page.js create mode 100644 scm-ui/src/createReduxStore.js create mode 100644 scm-ui/src/index.js create mode 100644 scm-ui/src/modules/page.js create mode 100644 scm-ui/src/registerServiceWorker.js create mode 100644 scm.iml diff --git a/.hgignore b/.hgignore index 71fbaa8735..4e95a4bedc 100644 --- a/.hgignore +++ b/.hgignore @@ -30,7 +30,8 @@ Desktop DF$ rebel.xml \.pyc # ui -scm-ui/node_modules/ +scm-ui/node_modules scm-ui/yarn.lock scm-ui/.gitignore scm-ui/package-lock.json +node_modules diff --git a/scm-ui/package.json b/scm-ui/package.json new file mode 100644 index 0000000000..c56f88f9e1 --- /dev/null +++ b/scm-ui/package.json @@ -0,0 +1,27 @@ +{ + "name": "scm-ui", + "version": "0.1.0", + "private": true, + "dependencies": { + "ces-theme": "https://github.com/cloudogu/ces-theme.git", + "classnames": "^2.2.5", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-jss": "^8.6.0", + "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-redux": "^5.0.0-alpha.9", + "redux-devtools-extension": "^2.13.5" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} diff --git a/scm-ui/public/favicon.ico b/scm-ui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/scm-ui/public/index.html b/scm-ui/public/index.html new file mode 100644 index 0000000000..ed0ebafa1b --- /dev/null +++ b/scm-ui/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + React App + + + +
+ + + diff --git a/scm-ui/public/manifest.json b/scm-ui/public/manifest.json new file mode 100644 index 0000000000..ef19ec243e --- /dev/null +++ b/scm-ui/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/scm-ui/src/App.js b/scm-ui/src/App.js new file mode 100644 index 0000000000..79083246bd --- /dev/null +++ b/scm-ui/src/App.js @@ -0,0 +1,20 @@ +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 ( +
+ +
+
+ ); + } +} + +export default withRouter(App); diff --git a/scm-ui/src/Main.js b/scm-ui/src/Main.js new file mode 100644 index 0000000000..068bc38a01 --- /dev/null +++ b/scm-ui/src/Main.js @@ -0,0 +1,36 @@ +//@flow +import React from 'react'; +import injectSheet from 'react-jss'; +import classNames from 'classnames'; + +import { Route, withRouter } from 'react-router'; + +import Page from './containers/Page'; +import {Switch} from 'react-router-dom'; + +const styles = { + content: { + paddingTop: '60px' + }, +}; + +type Props = { + classes: any +} + +class Main extends React.Component { + + render() { + const { classes } = this.props; + return ( +
+ + + +
+ ); + } + +} + +export default withRouter(injectSheet(styles)(Main)); diff --git a/scm-ui/src/Navigation.js b/scm-ui/src/Navigation.js new file mode 100644 index 0000000000..76dd76b268 --- /dev/null +++ b/scm-ui/src/Navigation.js @@ -0,0 +1,52 @@ +//@flow +import React from 'react'; +import {Link} from 'react-router-dom'; + +type Props = {}; + +type State = { + collapsed: boolean +}; + +class Navigation extends React.Component { + + constructor(props: Props) { + super(props); + this.state = { + collapsed: true + }; + } + + toggleCollapse = () => { + this.setState({ + collapsed: !this.state.collapsed + }); + }; + + render() { + + return ( + + ); + } + +} + +export default Navigation; diff --git a/scm-ui/src/containers/Page.js b/scm-ui/src/containers/Page.js new file mode 100644 index 0000000000..6594520ff4 --- /dev/null +++ b/scm-ui/src/containers/Page.js @@ -0,0 +1,3 @@ +/** + * Created by masuewer on 02.07.18. + */ diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js new file mode 100644 index 0000000000..9b53bb0f3d --- /dev/null +++ b/scm-ui/src/createReduxStore.js @@ -0,0 +1,22 @@ +import thunk from 'redux-thunk'; +import logger from 'redux-logger'; +import { createStore, compose, applyMiddleware, combineReducers } from 'redux'; +import { routerReducer, routerMiddleware } from 'react-router-redux'; + +import page from './modules/page'; + +function createReduxStore(history) { + const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + + const reducer = combineReducers({ + router: routerReducer, + page + }); + + return createStore( + reducer, + composeEnhancers(applyMiddleware(routerMiddleware(history), thunk, logger)) + ); +} + +export default createReduxStore; diff --git a/scm-ui/src/index.js b/scm-ui/src/index.js new file mode 100644 index 0000000000..12d8f58f94 --- /dev/null +++ b/scm-ui/src/index.js @@ -0,0 +1,32 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; +import registerServiceWorker from './registerServiceWorker'; + +import { Provider } from 'react-redux'; +import createHistory from 'history/createBrowserHistory'; +import createReduxStore from './createReduxStore'; +import { ConnectedRouter } from 'react-router-redux'; + + +// Create a history of your choosing (we're using a browser history in this case) +const history = createHistory({ + basename: process.env.PUBLIC_URL +}); + +window.appHistory = history; +// Add the reducer to your store on the `router` key +// Also apply our middleware for navigating +const store = createReduxStore(history); + +ReactDOM.render( + + { /* ConnectedRouter will use the store from Provider automatically */} + + + + , + document.getElementById('root') +); + +registerServiceWorker(); diff --git a/scm-ui/src/modules/page.js b/scm-ui/src/modules/page.js new file mode 100644 index 0000000000..6594520ff4 --- /dev/null +++ b/scm-ui/src/modules/page.js @@ -0,0 +1,3 @@ +/** + * Created by masuewer on 02.07.18. + */ diff --git a/scm-ui/src/registerServiceWorker.js b/scm-ui/src/registerServiceWorker.js new file mode 100644 index 0000000000..a3e6c0cfc1 --- /dev/null +++ b/scm-ui/src/registerServiceWorker.js @@ -0,0 +1,117 @@ +// In production, we register a service worker to serve assets from local cache. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on the "N+1" visit to a page, since previously +// cached resources are updated in the background. + +// To learn more about the benefits of this model, read https://goo.gl/KwvDNy. +// This link also includes instructions on opting out of this behavior. + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export default function register() { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Lets check if a service worker still exists or not. + checkValidServiceWorker(swUrl); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://goo.gl/SC7cgQ' + ); + }); + } else { + // Is not local host. Just register service worker + registerValidSW(swUrl); + } + }); + } +} + +function registerValidSW(swUrl) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the old content will have been purged and + // the fresh content will have been added to the cache. + // It's the perfect time to display a "New content is + // available; please refresh." message in your web app. + console.log('New content is available; please refresh.'); + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + if ( + response.status === 404 || + response.headers.get('content-type').indexOf('javascript') === -1 + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +} diff --git a/scm.iml b/scm.iml new file mode 100644 index 0000000000..a0d516ed9a --- /dev/null +++ b/scm.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file From e087920ba500ded56f075ecba0f7881bf0be9322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 2 Jul 2018 16:05:58 +0200 Subject: [PATCH 005/145] added mockup users page and login page --- scm-ui/src/Login.js | 36 +++++++++++++++++++ scm-ui/src/Main.js | 4 ++- scm-ui/src/containers/Page.js | 66 ++++++++++++++++++++++++++++++++-- scm-ui/src/containers/Users.js | 58 ++++++++++++++++++++++++++++++ scm-ui/src/createReduxStore.js | 4 ++- scm-ui/src/modules/page.js | 64 +++++++++++++++++++++++++++++++-- scm-ui/src/modules/users.js | 61 +++++++++++++++++++++++++++++++ 7 files changed, 285 insertions(+), 8 deletions(-) create mode 100644 scm-ui/src/Login.js create mode 100644 scm-ui/src/containers/Users.js create mode 100644 scm-ui/src/modules/users.js diff --git a/scm-ui/src/Login.js b/scm-ui/src/Login.js new file mode 100644 index 0000000000..d6ef5a29a1 --- /dev/null +++ b/scm-ui/src/Login.js @@ -0,0 +1,36 @@ +//@flow +import React from 'react'; +import injectSheet from 'react-jss'; + +const styles = { + wrapper: { + width: '100%', + display: 'flex', + height: '10em' + }, + loading: { + margin: 'auto', + textAlign: 'center' + } +}; + +type Props = { + classes: any; +} + +class Login extends React.Component { + + render() { + const { classes } = this.props; + return ( +
+
+ You need to log in! ... +
+
+ ); + } + +} + +export default injectSheet(styles)(Login); diff --git a/scm-ui/src/Main.js b/scm-ui/src/Main.js index 068bc38a01..2a6fe40ef3 100644 --- a/scm-ui/src/Main.js +++ b/scm-ui/src/Main.js @@ -6,6 +6,7 @@ import classNames from 'classnames'; import { Route, withRouter } from 'react-router'; import Page from './containers/Page'; +import Users from './containers/Users'; import {Switch} from 'react-router-dom'; const styles = { @@ -25,7 +26,8 @@ class Main extends React.Component { return (
- + +
); diff --git a/scm-ui/src/containers/Page.js b/scm-ui/src/containers/Page.js index 6594520ff4..f94914c2b8 100644 --- a/scm-ui/src/containers/Page.js +++ b/scm-ui/src/containers/Page.js @@ -1,3 +1,63 @@ -/** - * Created by masuewer on 02.07.18. - */ +// @flow +import React from 'react'; +import { connect } from 'react-redux'; + +import { fetchRepositoriesIfNeeded } from '../modules/page'; +import Login from '../Login'; + + +type Props = { + loading: boolean, + error: any, + repositories: any, + fetchRepositoriesIfNeeded: () => void +} + +class Page extends React.Component { + + componentDidMount() { + this.props.fetchRepositoriesIfNeeded(); + } + + render() { + const { loading, error, repositories } = this.props; + + + if(loading) { + return ( +
+

SCM

+ +
+ ); + } + else if(!loading){ + return ( +
+

SCM

+

Startpage

+
+ Users hier! + +
+ ); + } + + + } + +} + +const mapStateToProps = (state) => { + return null; +}; + +const mapDispatchToProps = (dispatch) => { + return { + fetchRepositoriesIfNeeded: () => { + dispatch(fetchRepositoriesIfNeeded()) + } + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Page); diff --git a/scm-ui/src/containers/Users.js b/scm-ui/src/containers/Users.js new file mode 100644 index 0000000000..00b22862f8 --- /dev/null +++ b/scm-ui/src/containers/Users.js @@ -0,0 +1,58 @@ +// @flow +import React from 'react'; +import { connect } from 'react-redux'; + +import { fetchRepositoriesIfNeeded } from '../modules/users'; +import Login from '../Login'; + +type Props = { + loading: boolean, + error: any, + repositories: any, + fetchRepositoriesIfNeeded: () => void +} + +class Users extends React.Component { + + componentDidMount() { + this.props.fetchRepositoriesIfNeeded(); + } + + render() { + const { loading, error, repositories } = this.props; + + + + if(loading) { + return ( +
+

SCM

+ +
+ ); + } + else if(!loading){ + return ( +
+

SCM

+

Users

+
+ ); + } + } + +} + +const mapStateToProps = (state) => { + return null; +}; + +const mapDispatchToProps = (dispatch) => { + return { + fetchRepositoriesIfNeeded: () => { + dispatch(fetchRepositoriesIfNeeded()) + } + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Users); diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index 9b53bb0f3d..244c5699b5 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -4,13 +4,15 @@ import { createStore, compose, applyMiddleware, combineReducers } from 'redux'; import { routerReducer, routerMiddleware } from 'react-router-redux'; import page from './modules/page'; +import users from './modules/users'; function createReduxStore(history) { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const reducer = combineReducers({ router: routerReducer, - page + page, + users }); return createStore( diff --git a/scm-ui/src/modules/page.js b/scm-ui/src/modules/page.js index 6594520ff4..fe8db685e0 100644 --- a/scm-ui/src/modules/page.js +++ b/scm-ui/src/modules/page.js @@ -1,3 +1,61 @@ -/** - * Created by masuewer on 02.07.18. - */ +//@flow +const FETCH_REPOSITORIES = 'smeagol/repositories/FETCH'; +const FETCH_REPOSITORIES_SUCCESS = 'smeagol/repositories/FETCH_SUCCESS'; +const FETCH_REPOSITORIES_FAILURE = 'smeagol/repositories/FETCH_FAILURE'; + +const THRESHOLD_TIMESTAMP = 10000; + +function requestRepositories() { + return { + type: FETCH_REPOSITORIES + }; +} + + +function fetchRepositories() { + return function(dispatch) { + dispatch(requestRepositories()); + return null; + } +} + +export function shouldFetchRepositories(state: any): boolean { + const repositories = state.repositories; + return null; +} + +export function fetchRepositoriesIfNeeded() { + return (dispatch, getState) => { + if (shouldFetchRepositories(getState())) { + dispatch(fetchRepositories()); + } + } +} + +export default function reducer(state = {}, action = {}) { + switch (action.type) { + case FETCH_REPOSITORIES: + return { + ...state, + loading: true, + error: null + }; + case FETCH_REPOSITORIES_SUCCESS: + return { + ...state, + loading: true, + timestamp: action.timestamp, + error: null, + repositories: action.payload + }; + case FETCH_REPOSITORIES_FAILURE: + return { + ...state, + loading: true, + error: action.payload + }; + + default: + return state + } +} diff --git a/scm-ui/src/modules/users.js b/scm-ui/src/modules/users.js new file mode 100644 index 0000000000..b482010b61 --- /dev/null +++ b/scm-ui/src/modules/users.js @@ -0,0 +1,61 @@ +//@flow +const FETCH_REPOSITORIES = 'smeagol/repositories/FETCH'; +const FETCH_REPOSITORIES_SUCCESS = 'smeagol/repositories/FETCH_SUCCESS'; +const FETCH_REPOSITORIES_FAILURE = 'smeagol/repositories/FETCH_FAILURE'; + +const THRESHOLD_TIMESTAMP = 10000; + +function requestRepositories() { + return { + type: FETCH_REPOSITORIES + }; +} + + +function fetchRepositories() { + return function(dispatch) { + dispatch(requestRepositories()); + return null; + } +} + +export function shouldFetchRepositories(state: any): boolean { + const repositories = state.repositories; + return null; +} + +export function fetchRepositoriesIfNeeded() { + return (dispatch, getState) => { + if (shouldFetchRepositories(getState())) { + dispatch(fetchRepositories()); + } + } +} + +export default function reducer(state = {}, action = {}) { + switch (action.type) { + case FETCH_REPOSITORIES: + return { + ...state, + loading: true, + error: null + }; + case FETCH_REPOSITORIES_SUCCESS: + return { + ...state, + loading: false, + timestamp: action.timestamp, + error: null, + repositories: action.payload + }; + case FETCH_REPOSITORIES_FAILURE: + return { + ...state, + loading: false, + error: action.payload + }; + + default: + return state + } +} From 69a081ccf8e6a574443089c15444149355d1adaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Mon, 2 Jul 2018 16:22:24 +0200 Subject: [PATCH 006/145] renaming: loading to login --- scm-ui/src/Login.js | 4 ++-- scm-ui/src/containers/Page.js | 8 ++++---- scm-ui/src/containers/Users.js | 8 ++++---- scm-ui/src/modules/page.js | 6 +++--- scm-ui/src/modules/users.js | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/scm-ui/src/Login.js b/scm-ui/src/Login.js index d6ef5a29a1..f85fbef9ed 100644 --- a/scm-ui/src/Login.js +++ b/scm-ui/src/Login.js @@ -8,7 +8,7 @@ const styles = { display: 'flex', height: '10em' }, - loading: { + login: { margin: 'auto', textAlign: 'center' } @@ -24,7 +24,7 @@ class Login extends React.Component { const { classes } = this.props; return (
-
+
You need to log in! ...
diff --git a/scm-ui/src/containers/Page.js b/scm-ui/src/containers/Page.js index f94914c2b8..7586be9167 100644 --- a/scm-ui/src/containers/Page.js +++ b/scm-ui/src/containers/Page.js @@ -7,7 +7,7 @@ import Login from '../Login'; type Props = { - loading: boolean, + login: boolean, error: any, repositories: any, fetchRepositoriesIfNeeded: () => void @@ -20,10 +20,10 @@ class Page extends React.Component { } render() { - const { loading, error, repositories } = this.props; + const { login, error, repositories } = this.props; - if(loading) { + if(login) { return (

SCM

@@ -31,7 +31,7 @@ class Page extends React.Component {
); } - else if(!loading){ + else if(!login){ return (

SCM

diff --git a/scm-ui/src/containers/Users.js b/scm-ui/src/containers/Users.js index 00b22862f8..8a4bfd5494 100644 --- a/scm-ui/src/containers/Users.js +++ b/scm-ui/src/containers/Users.js @@ -6,7 +6,7 @@ import { fetchRepositoriesIfNeeded } from '../modules/users'; import Login from '../Login'; type Props = { - loading: boolean, + login: boolean, error: any, repositories: any, fetchRepositoriesIfNeeded: () => void @@ -19,11 +19,11 @@ class Users extends React.Component { } render() { - const { loading, error, repositories } = this.props; + const { login, error, repositories } = this.props; - if(loading) { + if(login) { return (

SCM

@@ -31,7 +31,7 @@ class Users extends React.Component {
); } - else if(!loading){ + else if(!login){ return (

SCM

diff --git a/scm-ui/src/modules/page.js b/scm-ui/src/modules/page.js index fe8db685e0..58fd12ca18 100644 --- a/scm-ui/src/modules/page.js +++ b/scm-ui/src/modules/page.js @@ -37,13 +37,13 @@ export default function reducer(state = {}, action = {}) { case FETCH_REPOSITORIES: return { ...state, - loading: true, + login: true, error: null }; case FETCH_REPOSITORIES_SUCCESS: return { ...state, - loading: true, + login: true, timestamp: action.timestamp, error: null, repositories: action.payload @@ -51,7 +51,7 @@ export default function reducer(state = {}, action = {}) { case FETCH_REPOSITORIES_FAILURE: return { ...state, - loading: true, + login: true, error: action.payload }; diff --git a/scm-ui/src/modules/users.js b/scm-ui/src/modules/users.js index b482010b61..d4617c3a47 100644 --- a/scm-ui/src/modules/users.js +++ b/scm-ui/src/modules/users.js @@ -37,13 +37,13 @@ export default function reducer(state = {}, action = {}) { case FETCH_REPOSITORIES: return { ...state, - loading: true, + login: true, error: null }; case FETCH_REPOSITORIES_SUCCESS: return { ...state, - loading: false, + login: false, timestamp: action.timestamp, error: null, repositories: action.payload @@ -51,7 +51,7 @@ export default function reducer(state = {}, action = {}) { case FETCH_REPOSITORIES_FAILURE: return { ...state, - loading: false, + login: false, error: action.payload }; From 85902010ce1c4801005bd6d60b35d87e3bacc335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Wed, 4 Jul 2018 09:55:02 +0200 Subject: [PATCH 007/145] refactoring/renaming --- scm-ui/src/Main.js | 4 +-- .../containers/{Page.js => Repositories.js} | 8 ++--- scm-ui/src/containers/Users.js | 14 ++++---- scm-ui/src/createReduxStore.js | 4 +-- .../src/modules/{page.js => repositories.js} | 8 ++--- scm-ui/src/modules/users.js | 32 +++++++++---------- 6 files changed, 33 insertions(+), 37 deletions(-) rename scm-ui/src/containers/{Page.js => Repositories.js} (78%) rename scm-ui/src/modules/{page.js => repositories.js} (81%) diff --git a/scm-ui/src/Main.js b/scm-ui/src/Main.js index 2a6fe40ef3..10e7d4f0c2 100644 --- a/scm-ui/src/Main.js +++ b/scm-ui/src/Main.js @@ -5,7 +5,7 @@ import classNames from 'classnames'; import { Route, withRouter } from 'react-router'; -import Page from './containers/Page'; +import Repositories from './containers/Repositories'; import Users from './containers/Users'; import {Switch} from 'react-router-dom'; @@ -26,7 +26,7 @@ class Main extends React.Component { return (
- +
diff --git a/scm-ui/src/containers/Page.js b/scm-ui/src/containers/Repositories.js similarity index 78% rename from scm-ui/src/containers/Page.js rename to scm-ui/src/containers/Repositories.js index 7586be9167..cb4aca4373 100644 --- a/scm-ui/src/containers/Page.js +++ b/scm-ui/src/containers/Repositories.js @@ -2,18 +2,16 @@ import React from 'react'; import { connect } from 'react-redux'; -import { fetchRepositoriesIfNeeded } from '../modules/page'; +import { fetchRepositoriesIfNeeded } from '../modules/repositories'; import Login from '../Login'; type Props = { login: boolean, error: any, - repositories: any, - fetchRepositoriesIfNeeded: () => void } -class Page extends React.Component { +class Repositories extends React.Component { componentDidMount() { this.props.fetchRepositoriesIfNeeded(); @@ -60,4 +58,4 @@ const mapDispatchToProps = (dispatch) => { } }; -export default connect(mapStateToProps, mapDispatchToProps)(Page); +export default connect(mapStateToProps, mapDispatchToProps)(Repositories); diff --git a/scm-ui/src/containers/Users.js b/scm-ui/src/containers/Users.js index 8a4bfd5494..1e4b0bfba8 100644 --- a/scm-ui/src/containers/Users.js +++ b/scm-ui/src/containers/Users.js @@ -2,24 +2,24 @@ import React from 'react'; import { connect } from 'react-redux'; -import { fetchRepositoriesIfNeeded } from '../modules/users'; +import { fetchUsersIfNeeded } from '../modules/users'; import Login from '../Login'; type Props = { login: boolean, error: any, - repositories: any, - fetchRepositoriesIfNeeded: () => void + users: any, + fetchUsersIfNeeded: () => void } class Users extends React.Component { componentDidMount() { - this.props.fetchRepositoriesIfNeeded(); + this.props.fetchUsersIfNeeded(); } render() { - const { login, error, repositories } = this.props; + const { login, error, users } = this.props; @@ -49,8 +49,8 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { - fetchRepositoriesIfNeeded: () => { - dispatch(fetchRepositoriesIfNeeded()) + fetchUsersIfNeeded: () => { + dispatch(fetchUsersIfNeeded()) } } }; diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index 244c5699b5..5077df3e54 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -3,7 +3,7 @@ import logger from 'redux-logger'; import { createStore, compose, applyMiddleware, combineReducers } from 'redux'; import { routerReducer, routerMiddleware } from 'react-router-redux'; -import page from './modules/page'; +import repositories from './modules/repositories'; import users from './modules/users'; function createReduxStore(history) { @@ -11,7 +11,7 @@ function createReduxStore(history) { const reducer = combineReducers({ router: routerReducer, - page, + repositories, users }); diff --git a/scm-ui/src/modules/page.js b/scm-ui/src/modules/repositories.js similarity index 81% rename from scm-ui/src/modules/page.js rename to scm-ui/src/modules/repositories.js index 58fd12ca18..c58f613330 100644 --- a/scm-ui/src/modules/page.js +++ b/scm-ui/src/modules/repositories.js @@ -1,9 +1,7 @@ //@flow -const FETCH_REPOSITORIES = 'smeagol/repositories/FETCH'; -const FETCH_REPOSITORIES_SUCCESS = 'smeagol/repositories/FETCH_SUCCESS'; -const FETCH_REPOSITORIES_FAILURE = 'smeagol/repositories/FETCH_FAILURE'; - -const THRESHOLD_TIMESTAMP = 10000; +const FETCH_REPOSITORIES = 'scm/repositories/FETCH'; +const FETCH_REPOSITORIES_SUCCESS = 'scm/repositories/FETCH_SUCCESS'; +const FETCH_REPOSITORIES_FAILURE = 'scm/repositories/FETCH_FAILURE'; function requestRepositories() { return { diff --git a/scm-ui/src/modules/users.js b/scm-ui/src/modules/users.js index d4617c3a47..2afdfdfd8a 100644 --- a/scm-ui/src/modules/users.js +++ b/scm-ui/src/modules/users.js @@ -1,54 +1,54 @@ //@flow -const FETCH_REPOSITORIES = 'smeagol/repositories/FETCH'; -const FETCH_REPOSITORIES_SUCCESS = 'smeagol/repositories/FETCH_SUCCESS'; -const FETCH_REPOSITORIES_FAILURE = 'smeagol/repositories/FETCH_FAILURE'; +const FETCH_USERS = 'scm/users/FETCH'; +const FETCH_USERS_SUCCESS= 'scm/users/FETCH_SUCCESS'; +const FETCH_USERS_FAILURE = 'scm/users/FETCH_FAILURE'; const THRESHOLD_TIMESTAMP = 10000; -function requestRepositories() { +function requestUsers() { return { - type: FETCH_REPOSITORIES + type: FETCH_USERS }; } -function fetchRepositories() { +function fetchUsers() { return function(dispatch) { - dispatch(requestRepositories()); + dispatch(requestUsers()); return null; } } -export function shouldFetchRepositories(state: any): boolean { - const repositories = state.repositories; +export function shouldFetchUsers(state: any): boolean { + const users = state.users; return null; } -export function fetchRepositoriesIfNeeded() { +export function fetchUsersIfNeeded() { return (dispatch, getState) => { - if (shouldFetchRepositories(getState())) { - dispatch(fetchRepositories()); + if (shouldFetchUsers(getState())) { + dispatch(fetchUsers()); } } } export default function reducer(state = {}, action = {}) { switch (action.type) { - case FETCH_REPOSITORIES: + case FETCH_USERS: return { ...state, login: true, error: null }; - case FETCH_REPOSITORIES_SUCCESS: + case FETCH_USERS_SUCCESS: return { ...state, login: false, timestamp: action.timestamp, error: null, - repositories: action.payload + users: action.payload }; - case FETCH_REPOSITORIES_FAILURE: + case FETCH_USERS_FAILURE: return { ...state, login: false, From 3cc87ede734cee5d3586a4c1a969e0cc8ef75e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Wed, 4 Jul 2018 16:43:46 +0200 Subject: [PATCH 008/145] add restentpoint for login/logout, restructuring of modules and components, add flow usage --- .hgignore | 1 + pom.xml | 1 + scm-ui/flow-typed/npm/history_v4.x.x.js | 128 +++++++++ scm-ui/package.json | 21 +- scm-ui/pom.xml | 18 ++ scm-ui/src/App.js | 20 -- scm-ui/src/apiclient.js | 64 +++++ scm-ui/src/containers/App.js | 35 +++ scm-ui/src/{ => containers}/Login.js | 0 scm-ui/src/{ => containers}/Main.js | 4 +- scm-ui/src/{ => containers}/Navigation.js | 0 scm-ui/src/createReduxStore.js | 17 +- scm-ui/src/index.js | 41 +-- .../containers/Repositories.js | 20 +- .../modules/repositories.js | 1 - scm-ui/src/{ => users}/containers/Users.js | 14 +- scm-ui/src/{ => users}/modules/users.js | 3 - .../v2/resources/AuthenticationResource.java | 269 ++++++++++++++++++ .../java/sonia/scm/filter/SecurityFilter.java | 8 +- .../sonia/scm/security/SecurityRequests.java | 24 ++ .../web/security/ApiAuthenticationFilter.java | 19 +- .../scm/security/SecurityRequestsTest.java | 37 +++ 22 files changed, 646 insertions(+), 99 deletions(-) create mode 100644 scm-ui/flow-typed/npm/history_v4.x.x.js create mode 100644 scm-ui/pom.xml delete mode 100644 scm-ui/src/App.js create mode 100644 scm-ui/src/apiclient.js create mode 100644 scm-ui/src/containers/App.js rename scm-ui/src/{ => containers}/Login.js (100%) rename scm-ui/src/{ => containers}/Main.js (85%) rename scm-ui/src/{ => containers}/Navigation.js (100%) rename scm-ui/src/{ => repositories}/containers/Repositories.js (79%) rename scm-ui/src/{ => repositories}/modules/repositories.js (99%) rename scm-ui/src/{ => users}/containers/Users.js (81%) rename scm-ui/src/{ => users}/modules/users.js (96%) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java create mode 100644 scm-webapp/src/test/java/sonia/scm/security/SecurityRequestsTest.java diff --git a/.hgignore b/.hgignore index 4e95a4bedc..d7152e7a57 100644 --- a/.hgignore +++ b/.hgignore @@ -35,3 +35,4 @@ scm-ui/yarn.lock scm-ui/.gitignore scm-ui/package-lock.json node_modules +scm-ui/.flowconfig diff --git a/pom.xml b/pom.xml index 2416371d48..7ed637f0d7 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,7 @@ scm-test scm-plugins scm-dao-xml + scm-ui scm-webapp scm-server scm-clients diff --git a/scm-ui/flow-typed/npm/history_v4.x.x.js b/scm-ui/flow-typed/npm/history_v4.x.x.js new file mode 100644 index 0000000000..04061f029c --- /dev/null +++ b/scm-ui/flow-typed/npm/history_v4.x.x.js @@ -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) => void, + replace: (path: string, Array) => 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, + push: (path: string, Array) => void, + replace: (path: string, Array) => 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, + 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) => void, + replace: (path: string, Array) => 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; +} diff --git a/scm-ui/package.json b/scm-ui/package.json index c56f88f9e1..8513981bf5 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -3,25 +3,30 @@ "version": "0.1.0", "private": true, "dependencies": { - "ces-theme": "https://github.com/cloudogu/ces-theme.git", "classnames": "^2.2.5", + "flow-bin": "^0.75.0", + "history": "^4.7.2", "react": "^16.4.1", "react-dom": "^16.4.1", "react-jss": "^8.6.0", "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-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": { "start": "react-scripts start", "build": "react-scripts build", "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" } } diff --git a/scm-ui/pom.xml b/scm-ui/pom.xml new file mode 100644 index 0000000000..aa80884abc --- /dev/null +++ b/scm-ui/pom.xml @@ -0,0 +1,18 @@ + + + + 4.0.0 + + + sonia.scm + scm + 2.0.0-SNAPSHOT + + + sonia.scm.clients + scm-ui + pom + 2.0.0-SNAPSHOT + scm-ui + + diff --git a/scm-ui/src/App.js b/scm-ui/src/App.js deleted file mode 100644 index 79083246bd..0000000000 --- a/scm-ui/src/App.js +++ /dev/null @@ -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 ( -
- -
-
- ); - } -} - -export default withRouter(App); diff --git a/scm-ui/src/apiclient.js b/scm-ui/src/apiclient.js new file mode 100644 index 0000000000..f6b04a59a1 --- /dev/null +++ b/scm-ui/src/apiclient.js @@ -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(); diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js new file mode 100644 index 0000000000..e03221ec6a --- /dev/null +++ b/scm-ui/src/containers/App.js @@ -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 ( +
+ +
+ ); + } + else { + return ( +
+ +
+
+ ); + } + } +} + +export default withRouter(App); diff --git a/scm-ui/src/Login.js b/scm-ui/src/containers/Login.js similarity index 100% rename from scm-ui/src/Login.js rename to scm-ui/src/containers/Login.js diff --git a/scm-ui/src/Main.js b/scm-ui/src/containers/Main.js similarity index 85% rename from scm-ui/src/Main.js rename to scm-ui/src/containers/Main.js index 10e7d4f0c2..8f15455078 100644 --- a/scm-ui/src/Main.js +++ b/scm-ui/src/containers/Main.js @@ -5,8 +5,8 @@ import classNames from 'classnames'; import { Route, withRouter } from 'react-router'; -import Repositories from './containers/Repositories'; -import Users from './containers/Users'; +import Repositories from '../repositories/containers/Repositories'; +import Users from '../users/containers/Users'; import {Switch} from 'react-router-dom'; const styles = { diff --git a/scm-ui/src/Navigation.js b/scm-ui/src/containers/Navigation.js similarity index 100% rename from scm-ui/src/Navigation.js rename to scm-ui/src/containers/Navigation.js diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index 5077df3e54..502fc55faf 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -1,12 +1,15 @@ -import thunk from 'redux-thunk'; -import logger from 'redux-logger'; -import { createStore, compose, applyMiddleware, combineReducers } from 'redux'; -import { routerReducer, routerMiddleware } from 'react-router-redux'; +// @flow +import thunk from "redux-thunk"; +import logger from "redux-logger"; +import { createStore, compose, applyMiddleware, combineReducers } from "redux"; +import { routerReducer, routerMiddleware } from "react-router-redux"; -import repositories from './modules/repositories'; -import users from './modules/users'; +import repositories from "./repositories/modules/repositories"; +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 reducer = combineReducers({ diff --git a/scm-ui/src/index.js b/scm-ui/src/index.js index 12d8f58f94..564249494a 100644 --- a/scm-ui/src/index.js +++ b/scm-ui/src/index.js @@ -1,32 +1,41 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; -import registerServiceWorker from './registerServiceWorker'; +// @flow +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./containers/App"; +import registerServiceWorker from "./registerServiceWorker"; -import { Provider } from 'react-redux'; -import createHistory from 'history/createBrowserHistory'; -import createReduxStore from './createReduxStore'; -import { ConnectedRouter } from 'react-router-redux'; +import { Provider } from "react-redux"; +import createHistory from "history/createBrowserHistory"; +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) -const history = createHistory({ - basename: process.env.PUBLIC_URL +const history: BrowserHistory = createHistory({ + basename: publicUrl }); -window.appHistory = history; // Add the reducer to your store on the `router` key // Also apply our middleware for navigating const store = createReduxStore(history); +const root = document.getElementById("root"); +if (!root) { + throw new Error("could not find root element"); +} + ReactDOM.render( - { /* ConnectedRouter will use the store from Provider automatically */} - - - + {/* ConnectedRouter will use the store from Provider automatically */} + + + , - document.getElementById('root') + root ); registerServiceWorker(); diff --git a/scm-ui/src/containers/Repositories.js b/scm-ui/src/repositories/containers/Repositories.js similarity index 79% rename from scm-ui/src/containers/Repositories.js rename to scm-ui/src/repositories/containers/Repositories.js index cb4aca4373..5df04cbc4c 100644 --- a/scm-ui/src/containers/Repositories.js +++ b/scm-ui/src/repositories/containers/Repositories.js @@ -3,12 +3,14 @@ import React from 'react'; import { connect } from 'react-redux'; import { fetchRepositoriesIfNeeded } from '../modules/repositories'; -import Login from '../Login'; +import Login from '../../containers/Login'; type Props = { login: boolean, - error: any, + error: Error, + repositories: any, + fetchRepositoriesIfNeeded: () => void } class Repositories extends React.Component { @@ -21,16 +23,7 @@ class Repositories extends React.Component { const { login, error, repositories } = this.props; - if(login) { - return ( -
-

SCM

- -
- ); - } - else if(!login){ - return ( + return (

SCM

Startpage

@@ -38,8 +31,7 @@ class Repositories extends React.Component { Users hier!
- ); - } + ) } diff --git a/scm-ui/src/modules/repositories.js b/scm-ui/src/repositories/modules/repositories.js similarity index 99% rename from scm-ui/src/modules/repositories.js rename to scm-ui/src/repositories/modules/repositories.js index c58f613330..e4ab290a44 100644 --- a/scm-ui/src/modules/repositories.js +++ b/scm-ui/src/repositories/modules/repositories.js @@ -1,4 +1,3 @@ -//@flow const FETCH_REPOSITORIES = 'scm/repositories/FETCH'; const FETCH_REPOSITORIES_SUCCESS = 'scm/repositories/FETCH_SUCCESS'; const FETCH_REPOSITORIES_FAILURE = 'scm/repositories/FETCH_FAILURE'; diff --git a/scm-ui/src/containers/Users.js b/scm-ui/src/users/containers/Users.js similarity index 81% rename from scm-ui/src/containers/Users.js rename to scm-ui/src/users/containers/Users.js index 1e4b0bfba8..e3f3c5190d 100644 --- a/scm-ui/src/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -3,7 +3,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { fetchUsersIfNeeded } from '../modules/users'; -import Login from '../Login'; +import Login from '../../containers/Login'; type Props = { login: boolean, @@ -22,23 +22,13 @@ class Users extends React.Component { const { login, error, users } = this.props; - - if(login) { - return ( -
-

SCM

- -
- ); - } - else if(!login){ return (

SCM

Users

); - } + } } diff --git a/scm-ui/src/modules/users.js b/scm-ui/src/users/modules/users.js similarity index 96% rename from scm-ui/src/modules/users.js rename to scm-ui/src/users/modules/users.js index 2afdfdfd8a..ed92f7ba10 100644 --- a/scm-ui/src/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -1,10 +1,7 @@ -//@flow const FETCH_USERS = 'scm/users/FETCH'; const FETCH_USERS_SUCCESS= 'scm/users/FETCH_SUCCESS'; const FETCH_USERS_FAILURE = 'scm/users/FETCH_FAILURE'; -const THRESHOLD_TIMESTAMP = 10000; - function requestUsers() { return { type: FETCH_USERS diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java new file mode 100644 index 0000000000..ed9733fdef --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthenticationResource.java @@ -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 scope; + + public String getGrantType() { + return grantType; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public boolean isCookie() { + return cookie; + } + + public List 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; + } + + } + +} 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 e94b6a3aee..07475e5853 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java @@ -44,6 +44,7 @@ import org.apache.shiro.subject.Subject; import sonia.scm.Priority; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.SecurityRequests; import sonia.scm.web.filter.HttpFilter; import sonia.scm.web.filter.SecurityHttpServletRequestWrapper; @@ -72,6 +73,8 @@ public class SecurityFilter extends HttpFilter /** Field description */ public static final String URL_AUTHENTICATION = "/api/rest/auth"; + public static final String URLV2_AUTHENTICATION = "/api/rest/v2/auth"; + //~--- constructors --------------------------------------------------------- /** @@ -104,10 +107,7 @@ public class SecurityFilter extends HttpFilter HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - String uri = - request.getRequestURI().substring(request.getContextPath().length()); - - if (!uri.startsWith(URL_AUTHENTICATION)) + if (!SecurityRequests.isAuthenticationRequest(request)) { Subject subject = SecurityUtils.getSubject(); if (hasPermission(subject)) diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java new file mode 100644 index 0000000000..225767cd3b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java @@ -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(); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java b/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java index 8340225872..d8fe469af9 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java @@ -36,24 +36,22 @@ package sonia.scm.web.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.Inject; - import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; import sonia.scm.filter.Filters; import sonia.scm.filter.WebElement; -import sonia.scm.web.filter.AuthenticationFilter; +import sonia.scm.security.SecurityRequests; import sonia.scm.web.WebTokenGenerator; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; +import sonia.scm.web.filter.AuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; 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. @@ -66,9 +64,6 @@ import javax.servlet.http.HttpServletResponse; public class ApiAuthenticationFilter extends AuthenticationFilter { - /** login uri */ - public static final String URI_LOGIN = "/api/rest/auth/access_token"; - //~--- constructors --------------------------------------------------------- /** @@ -104,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter throws IOException, ServletException { // skip filter on login resource - if (request.getRequestURI().contains(URI_LOGIN)) + if (SecurityRequests.isAuthenticationRequest(request)) { chain.doFilter(request, response); } diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestsTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestsTest.java new file mode 100644 index 0000000000..9e6d54dc0b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestsTest.java @@ -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")); + } +} From fbfebe1df7fcfdc2d13d4ecba6bd82347a0e6b43 Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Thu, 5 Jul 2018 16:48:56 +0200 Subject: [PATCH 009/145] Bootstrapped login in UI --- scm-ui/src/containers/App.js | 25 ++++++------ scm-ui/src/containers/Login.js | 70 ++++++++++++++++++++++++++------- scm-ui/src/createReduxStore.js | 9 +++-- scm-ui/src/modules/login.js | 71 ++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 30 deletions(-) create mode 100644 scm-ui/src/modules/login.js diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index e03221ec6a..b30ca75d0d 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -6,29 +6,26 @@ import { withRouter } from "react-router-dom"; type Props = { login: boolean -} +}; class App extends Component { - render() { + const { login } = this.props; - const { login} = this.props; - - if(login) { + if (!login) { return (
- + +
+ ); + } else { + return ( +
+ +
); } - else { - return ( -
- -
-
- ); - } } } diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index f85fbef9ed..fb20d48366 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -1,24 +1,35 @@ //@flow -import React from 'react'; -import injectSheet from 'react-jss'; +import React from "react"; +import injectSheet from "react-jss"; +import { login } from "../modules/login"; +import { connect } from "react-redux"; const styles = { wrapper: { - width: '100%', - display: 'flex', - height: '10em' + width: "100%", + display: "flex", + height: "10em" }, login: { - margin: 'auto', - textAlign: 'center' + margin: "auto", + textAlign: "center" } }; -type Props = { - classes: any; -} - class Login extends React.Component { + state = {}; + handleUsernameChange(event) { + this.setState({ username: event.target.value }); + } + + handlePasswordChange(event) { + this.setState({ password: event.target.value }); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.login(this.state.username, this.state.password); + } render() { const { classes } = this.props; @@ -26,11 +37,44 @@ class Login extends React.Component {
You need to log in! ... +
+ + + +
); } - } -export default injectSheet(styles)(Login); +const mapStateToProps = state => { + return {}; +}; + +const mapDispatchToProps = dispatch => { + return { + login: (username: string, password: string) => + dispatch(login(username, password)) + }; +}; + +const StyledLogin = injectSheet(styles)( + connect( + mapStateToProps, + mapDispatchToProps + )(Login) +); +export default StyledLogin; +// export default connect( +// mapStateToProps, +// mapDispatchToProps +// )(StyledLogin); diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index 502fc55faf..df46c5be34 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -6,16 +6,19 @@ import { routerReducer, routerMiddleware } from "react-router-redux"; import repositories from "./repositories/modules/repositories"; import users from "./users/modules/users"; +import login from "./modules/login"; -import type {BrowserHistory} from "history/createBrowserHistory"; +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({ router: routerReducer, repositories, - users + users, + login }); return createStore( diff --git a/scm-ui/src/modules/login.js b/scm-ui/src/modules/login.js new file mode 100644 index 0000000000..e46abc44f4 --- /dev/null +++ b/scm-ui/src/modules/login.js @@ -0,0 +1,71 @@ +//@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"; + +export function loginRequest() { + return { + type: LOGIN_REQUEST + }; +} + +export function login(username: string, password: string) { + var login_data = { + cookie: true, + grant_type: "password", + password: username, + username: password + }; + console.log(login_data); + return function(dispatch) { + dispatch(loginRequest()); + return fetch("/api/rest/v2/auth/access_token", { + method: "POST", + headers: { + "Content-Type": "application/json; charset=utf-8" + }, + body: JSON.stringify(login_data) + }).then( + response => { + if (response.ok) { + dispatch(loginSuccessful()); + } + }, + error => console.log("error logging in: " + error) + ); + }; +} + +export function loginSuccessful() { + return { + type: LOGIN_SUCCESSFUL + }; +} + +export default function reducer(state = {}, action = {}) { + switch (action.type) { + case LOGIN: + return { + ...state, + login: false, + error: null + }; + case LOGIN_SUCCESSFUL: + return { + ...state, + login: true, + error: null + }; + case LOGIN_FAILED: + return { + ...state, + login: false, + error: action.payload + }; + + default: + return state; + } +} From 643e6693b6fa626e4c14416bccfaa4e19113f85e Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Mon, 9 Jul 2018 11:38:13 +0200 Subject: [PATCH 010/145] 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; } From 3c3c2647b91bf327aaee012ee410fcc277dde107 Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Mon, 9 Jul 2018 11:41:23 +0200 Subject: [PATCH 011/145] Removed flow from yarn test --- scm-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/package.json b/scm-ui/package.json index 145839607c..c91b237aa5 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -22,7 +22,7 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "yarn flow && jest", + "test": "jest", "eject": "react-scripts eject", "flow": "flow" }, From 06ca71ce8683d5a65ced7f8626abe926c13e2eca Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Tue, 10 Jul 2018 08:38:38 +0200 Subject: [PATCH 012/145] Added bootstrapping for user table --- scm-ui/src/users/containers/UserRow.js | 20 +++++++ scm-ui/src/users/containers/Users.js | 74 +++++++++++++++++--------- scm-ui/src/users/modules/users.js | 34 +++++++++--- 3 files changed, 95 insertions(+), 33 deletions(-) create mode 100644 scm-ui/src/users/containers/UserRow.js diff --git a/scm-ui/src/users/containers/UserRow.js b/scm-ui/src/users/containers/UserRow.js new file mode 100644 index 0000000000..d9184d3586 --- /dev/null +++ b/scm-ui/src/users/containers/UserRow.js @@ -0,0 +1,20 @@ +// @flow +import React from "react"; + +type Props = { + user: any +}; + +export default class UserRow extends React.Component { + render() { + return ( + + {this.props.user.displayName} + {this.props.user.mail} + + + + + ); + } +} diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index e3f3c5190d..9a628c6fc2 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -1,48 +1,70 @@ // @flow -import React from 'react'; -import { connect } from 'react-redux'; +import React from "react"; +import { connect } from "react-redux"; -import { fetchUsersIfNeeded } from '../modules/users'; -import Login from '../../containers/Login'; +import { fetchUsersIfNeeded, fetchUsers } from "../modules/users"; +import Login from "../../containers/Login"; +import UserRow from "./UserRow"; type Props = { login: boolean, error: any, users: any, - fetchUsersIfNeeded: () => void -} + fetchUsersIfNeeded: () => void, + fetchUsers: () => void +}; class Users extends React.Component { - - componentDidMount() { - this.props.fetchUsersIfNeeded(); + componentWillMount() { + this.props.fetchUsers(); } render() { - const { login, error, users } = this.props; - - + if (this.props.users) { return (

SCM

Users

+ + + + + + + + + + {this.props.users.map((user, index) => { + return ; + })} + +
NameE-MailAdmin
); - - } - -} - -const mapStateToProps = (state) => { - return null; -}; - -const mapDispatchToProps = (dispatch) => { - return { - fetchUsersIfNeeded: () => { - dispatch(fetchUsersIfNeeded()) + } else { + return
Loading...
; } } +} + +const mapStateToProps = state => { + return { + users: state.users.users + }; }; -export default connect(mapStateToProps, mapDispatchToProps)(Users); +const mapDispatchToProps = dispatch => { + return { + fetchUsersIfNeeded: () => { + dispatch(fetchUsersIfNeeded()); + }, + fetchUsers: () => { + dispatch(fetchUsers()); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Users); diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index c1103d60f3..192e13e0eb 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -4,16 +4,38 @@ const FETCH_USERS = "scm/users/FETCH"; const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS"; const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE"; +const USERS_URL = "/scm/api/rest/v2/users"; + function requestUsers() { return { type: FETCH_USERS }; } -function fetchUsers() { +export function fetchUsers() { return function(dispatch) { - dispatch(requestUsers()); - return null; + // dispatch(requestUsers()); + return fetch(USERS_URL, { + credentials: "same-origin", + headers: { + Cache: "no-cache" + } + }) + .then(response => { + if (response.ok) { + return response.json(); + } + }) + .then(data => { + dispatch(fetchUsersSuccess(data)); + }); + }; +} + +function fetchUsersSuccess(users: any) { + return { + type: FETCH_USERS_SUCCESS, + payload: users }; } @@ -35,16 +57,14 @@ export default function reducer(state: any = {}, action: any = {}) { case FETCH_USERS: return { ...state, - login: true, - error: null + users: [{ name: "" }] }; case FETCH_USERS_SUCCESS: return { ...state, - login: false, timestamp: action.timestamp, error: null, - users: action.payload + users: action.payload._embedded.users }; case FETCH_USERS_FAILURE: return { From 1e86353a823f9a6f80b7e7fa2d6f3f53aa3fea5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Tue, 10 Jul 2018 08:55:53 +0200 Subject: [PATCH 013/145] use link component instead of --- scm-ui/src/repositories/containers/Repositories.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scm-ui/src/repositories/containers/Repositories.js b/scm-ui/src/repositories/containers/Repositories.js index 5df04cbc4c..a7cd59e7de 100644 --- a/scm-ui/src/repositories/containers/Repositories.js +++ b/scm-ui/src/repositories/containers/Repositories.js @@ -3,7 +3,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { fetchRepositoriesIfNeeded } from '../modules/repositories'; -import Login from '../../containers/Login'; +import { Link } from 'react-router-dom' type Props = { @@ -27,9 +27,7 @@ class Repositories extends React.Component { ) From 0648586092165238dbee003fcd0ad2bb9a67f672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Tue, 10 Jul 2018 15:18:37 +0200 Subject: [PATCH 014/145] mockup delete button for user --- scm-ui/src/apiclient.js | 8 +- .../repositories/containers/Repositories.js | 2 +- .../src/users/containers/DeleteUserButton.js | 46 +++++++++ scm-ui/src/users/containers/UserRow.js | 8 ++ scm-ui/src/users/containers/Users.js | 10 +- scm-ui/src/users/modules/users.js | 95 ++++++++++++++++--- 6 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 scm-ui/src/users/containers/DeleteUserButton.js diff --git a/scm-ui/src/apiclient.js b/scm-ui/src/apiclient.js index f6b04a59a1..02d3263fec 100644 --- a/scm-ui/src/apiclient.js +++ b/scm-ui/src/apiclient.js @@ -1,16 +1,14 @@ // @flow // get api base url from environment -const apiUrl = process.env.API_URL || process.env.PUBLIC_URL || ""; +const apiUrl = process.env.API_URL || process.env.PUBLIC_URL || "/scm"; 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" + Cache: "no-cache" } }; @@ -40,7 +38,7 @@ class ApiClient { return this.httpRequestWithJSONBody(url, payload, "POST"); } - delete(url: string, payload: any) { + delete(url: string) { let options: RequestOptions = { method: "DELETE" }; diff --git a/scm-ui/src/repositories/containers/Repositories.js b/scm-ui/src/repositories/containers/Repositories.js index a7cd59e7de..b1f01c5b5b 100644 --- a/scm-ui/src/repositories/containers/Repositories.js +++ b/scm-ui/src/repositories/containers/Repositories.js @@ -3,7 +3,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { fetchRepositoriesIfNeeded } from '../modules/repositories'; -import { Link } from 'react-router-dom' +import { Link } from 'react-router-dom'; type Props = { diff --git a/scm-ui/src/users/containers/DeleteUserButton.js b/scm-ui/src/users/containers/DeleteUserButton.js new file mode 100644 index 0000000000..195e603ae8 --- /dev/null +++ b/scm-ui/src/users/containers/DeleteUserButton.js @@ -0,0 +1,46 @@ +// @flow +import React from "react"; +import { deleteUser } from '../modules/users'; +import {connect} from "react-redux"; + +type Props = { + user: any, + deleteUser: (username: string) => void +}; + +class DeleteUser extends React.Component { + + deleteUser = () => { + this.props.deleteUser(this.props.user.name); + }; + + render() { + if(this.props.user._links.delete) { + return ( + + + ); + } + } +} + +const mapStateToProps = state => { + return { + users: state.users.users + }; +}; + +const mapDispatchToProps = dispatch => { + return { + deleteUser: (username: string) => { + dispatch(deleteUser(username)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DeleteUser); diff --git a/scm-ui/src/users/containers/UserRow.js b/scm-ui/src/users/containers/UserRow.js index d9184d3586..077ffa7fc3 100644 --- a/scm-ui/src/users/containers/UserRow.js +++ b/scm-ui/src/users/containers/UserRow.js @@ -1,11 +1,15 @@ // @flow import React from "react"; +import DeleteUserButton from "./DeleteUserButton"; type Props = { user: any }; + + export default class UserRow extends React.Component { + render() { return ( @@ -14,7 +18,11 @@ export default class UserRow extends React.Component { + + + + ); } } diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 9a628c6fc2..0702a56245 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -11,12 +11,18 @@ type Props = { error: any, users: any, fetchUsersIfNeeded: () => void, - fetchUsers: () => void + fetchUsers: () => void, + fetchUsersIfNeeded: (url: string) => void, + }; class Users extends React.Component { componentWillMount() { - this.props.fetchUsers(); + this.props.fetchUsersIfNeeded(); + } + + componentDidUpdate() { + this.props.fetchUsersIfNeeded(); } render() { diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index 192e13e0eb..a2a157d1c9 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -1,10 +1,16 @@ // @flow +import {apiClient, PAGE_NOT_FOUND_ERROR} from '../../apiclient'; const FETCH_USERS = "scm/users/FETCH"; const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS"; const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE"; +const FETCH_USERS_NOTFOUND = 'scm/users/FETCH_NOTFOUND'; -const USERS_URL = "/scm/api/rest/v2/users"; +const DELETE_USER = "scm/users/DELETE"; +const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS"; +const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE"; + +const USERS_URL = "users"; function requestUsers() { return { @@ -12,15 +18,29 @@ function requestUsers() { }; } +function failedToFetchUsers(url: string, err: Error) { + return { + type: FETCH_USERS_FAILURE, + payload: err, + url + }; +} + +function usersNotFound(url: string) { + return { + type: FETCH_USERS_NOTFOUND, + url + }; +} + export function fetchUsers() { + return function(dispatch) { - // dispatch(requestUsers()); - return fetch(USERS_URL, { - credentials: "same-origin", - headers: { - Cache: "no-cache" - } - }) + dispatch(requestUsers()); + return apiClient.get(USERS_URL) + .then(response => { + return response; + }) .then(response => { if (response.ok) { return response.json(); @@ -28,8 +48,15 @@ export function fetchUsers() { }) .then(data => { dispatch(fetchUsersSuccess(data)); + }) + .catch((err) => { + if (err === PAGE_NOT_FOUND_ERROR) { + dispatch(usersNotFound(USERS_URL)); + } else { + dispatch(failedToFetchUsers(USERS_URL, err)); + } }); - }; + } } function fetchUsersSuccess(users: any) { @@ -40,8 +67,10 @@ function fetchUsersSuccess(users: any) { } export function shouldFetchUsers(state: any): boolean { - const users = state.users; - return null; + if(state.users.users == null){ + return true; + } + return false; } export function fetchUsersIfNeeded() { @@ -52,17 +81,52 @@ export function fetchUsersIfNeeded() { }; } + + +function requestDeleteUser(url: string) { + return { + type: DELETE_USER, + url + }; +} + +function deleteUserSuccess() { + return { + type: DELETE_USER_SUCCESS, + }; +} + +function deleteUserFailure(url: string, err: Error) { + return { + type: DELETE_USER_FAILURE, + payload: err, + url + }; +} + +export function deleteUser(username: string) { + return function(dispatch) { + dispatch(requestDeleteUser(username)); + return apiClient.delete(USERS_URL + '/' + username) + .then(() => { + dispatch(deleteUserSuccess()); + }) + .catch((err) => dispatch(deleteUserFailure(username, err))); + } +} + + + export default function reducer(state: any = {}, action: any = {}) { switch (action.type) { case FETCH_USERS: return { ...state, - users: [{ name: "" }] + users: null }; case FETCH_USERS_SUCCESS: return { ...state, - timestamp: action.timestamp, error: null, users: action.payload._embedded.users }; @@ -72,6 +136,11 @@ export default function reducer(state: any = {}, action: any = {}) { login: false, error: action.payload }; + case DELETE_USER_SUCCESS: + return { + ...state, + users: null + }; default: return state; From 4af1842ea3ae48b505c1ed25106fd02edc0cd2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Tue, 10 Jul 2018 16:37:01 +0200 Subject: [PATCH 015/145] change password and username --- scm-ui/src/modules/login.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/modules/login.js b/scm-ui/src/modules/login.js index 53e2049ec8..13df980e93 100644 --- a/scm-ui/src/modules/login.js +++ b/scm-ui/src/modules/login.js @@ -66,8 +66,8 @@ export function login(username: string, password: string) { var login_data = { cookie: true, grant_type: "password", - password: username, - username: password + username, + password, }; return function(dispatch) { dispatch(loginRequest()); From af0de44172a44fc8d03adb203fcd9af48a81baf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Tue, 10 Jul 2018 16:37:27 +0200 Subject: [PATCH 016/145] deleteuserbutton is only a component --- .../src/users/containers/DeleteUserButton.js | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/scm-ui/src/users/containers/DeleteUserButton.js b/scm-ui/src/users/containers/DeleteUserButton.js index 195e603ae8..c26dc56fb6 100644 --- a/scm-ui/src/users/containers/DeleteUserButton.js +++ b/scm-ui/src/users/containers/DeleteUserButton.js @@ -1,46 +1,36 @@ // @flow import React from "react"; -import { deleteUser } from '../modules/users'; -import {connect} from "react-redux"; type Props = { user: any, - deleteUser: (username: string) => void + deleteUser: (link: string) => void }; class DeleteUser extends React.Component { deleteUser = () => { - this.props.deleteUser(this.props.user.name); + this.props.deleteUser(this.props.user._links.delete.href); + }; + + if(deleteButtonClicked) { + let deleteButtonAsk =
You really want to remove this user?
+ } + + isDeletable = () => { + return this.props.user._links.delete; }; render() { - if(this.props.user._links.delete) { - return ( - - - ); + if (!this.isDeletable()) { + return; } + return ( + + + ); } } -const mapStateToProps = state => { - return { - users: state.users.users - }; -}; - -const mapDispatchToProps = dispatch => { - return { - deleteUser: (username: string) => { - dispatch(deleteUser(username)); - } - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(DeleteUser); +export default DeleteUser; From 56848400217b3044c0c4afcdd38b2c6a40108bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Tue, 10 Jul 2018 16:37:40 +0200 Subject: [PATCH 017/145] add tests for view of delete button --- scm-ui/package.json | 5 +- .../users/containers/DeleteUserButton.test.js | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 scm-ui/src/users/containers/DeleteUserButton.test.js diff --git a/scm-ui/package.json b/scm-ui/package.json index c91b237aa5..84776009b5 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -32,7 +32,10 @@ } }, "devDependencies": { - "prettier": "^1.13.7" + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", + "prettier": "^1.13.7", + "react-test-renderer": "^16.4.1" }, "babel": { "presets": [ diff --git a/scm-ui/src/users/containers/DeleteUserButton.test.js b/scm-ui/src/users/containers/DeleteUserButton.test.js new file mode 100644 index 0000000000..efda995c86 --- /dev/null +++ b/scm-ui/src/users/containers/DeleteUserButton.test.js @@ -0,0 +1,54 @@ +import React from 'react'; +import {configure, shallow} from 'enzyme'; +import DeleteUserButton from "./DeleteUserButton"; +import Adapter from 'enzyme-adapter-react-16'; + +import 'raf/polyfill'; + +configure({ adapter: new Adapter() }); + +it('should render nothing, if the delete link is missing', () => { + + const user = { + _links: {} + }; + + const button = shallow(); + expect(button.text()).toBe(""); +}); + +it('should render the button', () => { + + const user = { + _links: { + "delete": { + "href": "/users" + } + } + }; + + const button = shallow(); + expect(button.text()).not.toBe(""); +}); + +it('should call the delete user function with delete url', () => { + + const user = { + _links: { + "delete": { + "href": "/users" + } + } + }; + + let calledUrl = null; + + function capture(url) { + calledUrl = url; + } + + const button = shallow(); + button.simulate("click"); + + expect(calledUrl).toBe("/users"); +}); From eb99eab354a43ce5acfd368d5fcd6f288eb44511 Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Tue, 10 Jul 2018 16:52:23 +0200 Subject: [PATCH 018/145] Use apiClient --- scm-ui/src/containers/Login.js | 24 +++++++++--- scm-ui/src/modules/login.js | 52 +++++++++----------------- scm-ui/src/users/containers/UserRow.js | 6 +-- scm-ui/src/users/modules/users.js | 29 +++++++------- 4 files changed, 51 insertions(+), 60 deletions(-) diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index e811927831..d7c3e0fba7 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -16,17 +16,31 @@ const styles = { } }; -class Login extends React.Component { - state = {}; - handleUsernameChange(event) { +type Props = { + classes: any, + login: (username: string, password: string) => void +}; + +type State = { + username: string, + password: string +}; + +class Login extends React.Component { + constructor(props: Props) { + super(props); + this.state = { username: "", password: "" }; + } + + handleUsernameChange(event: SyntheticInputEvent) { this.setState({ username: event.target.value }); } - handlePasswordChange(event) { + handlePasswordChange(event: SyntheticInputEvent) { this.setState({ password: event.target.value }); } - handleSubmit(event) { + handleSubmit(event: Event) { event.preventDefault(); this.props.login(this.state.username, this.state.password); } diff --git a/scm-ui/src/modules/login.js b/scm-ui/src/modules/login.js index 53e2049ec8..d3fc8c64e2 100644 --- a/scm-ui/src/modules/login.js +++ b/scm-ui/src/modules/login.js @@ -1,7 +1,10 @@ //@flow -const LOGIN_URL = "/scm/api/rest/v2/auth/access_token"; -const AUTHENTICATION_INFO_URL = "/scm/api/rest/v2/me"; +import { apiClient } from "../apiclient"; +import { isRegExp } from "util"; + +const LOGIN_URL = "/auth/access_token"; +const AUTHENTICATION_INFO_URL = "/me"; export const LOGIN = "scm/auth/login"; export const LOGIN_REQUEST = "scm/auth/login_request"; @@ -19,21 +22,12 @@ export function getIsAuthenticatedRequest() { } export function getIsAuthenticated() { - return function(dispatch) { + return function(dispatch: any) { dispatch(getIsAuthenticatedRequest()); - - return fetch(AUTHENTICATION_INFO_URL, { - credentials: "same-origin", - headers: { - Cache: "no-cache" - } - }) + return apiClient + .get(AUTHENTICATION_INFO_URL) .then(response => { - if (response.ok) { - return response.json(); - } else { - dispatch(isNotAuthenticated()); - } + return response.json(); }) .then(data => { if (data) { @@ -66,27 +60,17 @@ export function login(username: string, password: string) { var login_data = { cookie: true, grant_type: "password", - password: username, - username: password + username, + password }; - return function(dispatch) { + return function(dispatch: any) { dispatch(loginRequest()); - 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()); - } - }, - error => console.log("error logging in: " + error) - ); + return apiClient.post(LOGIN_URL, login_data).then(response => { + if (response.ok) { + dispatch(getIsAuthenticated()); + dispatch(loginSuccessful()); + } + }); }; } diff --git a/scm-ui/src/users/containers/UserRow.js b/scm-ui/src/users/containers/UserRow.js index 077ffa7fc3..5431178ba9 100644 --- a/scm-ui/src/users/containers/UserRow.js +++ b/scm-ui/src/users/containers/UserRow.js @@ -6,10 +6,7 @@ type Props = { user: any }; - - export default class UserRow extends React.Component { - render() { return ( @@ -19,10 +16,9 @@ export default class UserRow extends React.Component { - + - ); } } diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index a2a157d1c9..1fdc499535 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -1,10 +1,10 @@ // @flow -import {apiClient, PAGE_NOT_FOUND_ERROR} from '../../apiclient'; +import { apiClient, PAGE_NOT_FOUND_ERROR } from "../../apiclient"; const FETCH_USERS = "scm/users/FETCH"; const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS"; const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE"; -const FETCH_USERS_NOTFOUND = 'scm/users/FETCH_NOTFOUND'; +const FETCH_USERS_NOTFOUND = "scm/users/FETCH_NOTFOUND"; const DELETE_USER = "scm/users/DELETE"; const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS"; @@ -34,10 +34,10 @@ function usersNotFound(url: string) { } export function fetchUsers() { - - return function(dispatch) { + return function(dispatch: any => void) { dispatch(requestUsers()); - return apiClient.get(USERS_URL) + return apiClient + .get(USERS_URL) .then(response => { return response; }) @@ -49,14 +49,14 @@ export function fetchUsers() { .then(data => { dispatch(fetchUsersSuccess(data)); }) - .catch((err) => { + .catch(err => { if (err === PAGE_NOT_FOUND_ERROR) { dispatch(usersNotFound(USERS_URL)); } else { dispatch(failedToFetchUsers(USERS_URL, err)); } }); - } + }; } function fetchUsersSuccess(users: any) { @@ -67,7 +67,7 @@ function fetchUsersSuccess(users: any) { } export function shouldFetchUsers(state: any): boolean { - if(state.users.users == null){ + if (state.users.users == null) { return true; } return false; @@ -81,8 +81,6 @@ export function fetchUsersIfNeeded() { }; } - - function requestDeleteUser(url: string) { return { type: DELETE_USER, @@ -92,7 +90,7 @@ function requestDeleteUser(url: string) { function deleteUserSuccess() { return { - type: DELETE_USER_SUCCESS, + type: DELETE_USER_SUCCESS }; } @@ -107,16 +105,15 @@ function deleteUserFailure(url: string, err: Error) { export function deleteUser(username: string) { return function(dispatch) { dispatch(requestDeleteUser(username)); - return apiClient.delete(USERS_URL + '/' + username) + return apiClient + .delete(USERS_URL + "/" + username) .then(() => { dispatch(deleteUserSuccess()); }) - .catch((err) => dispatch(deleteUserFailure(username, err))); - } + .catch(err => dispatch(deleteUserFailure(username, err))); + }; } - - export default function reducer(state: any = {}, action: any = {}) { switch (action.type) { case FETCH_USERS: From e3caa93aa72895f66ccf0873563d090aeda0b1c7 Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Wed, 11 Jul 2018 12:02:53 +0200 Subject: [PATCH 019/145] Improved flow coverage, fixed bugs and enabled deleting users --- scm-ui/package.json | 3 +- scm-ui/src/apiclient.js | 16 +++++-- scm-ui/src/containers/App.js | 16 ++++--- scm-ui/src/modules/login.js | 25 ++++++++--- scm-ui/src/types/hal.js | 6 +++ .../src/users/containers/DeleteUserButton.js | 9 +--- .../users/containers/DeleteUserButton.test.js | 31 ++++++------- scm-ui/src/users/containers/UserRow.js | 14 +++--- scm-ui/src/users/containers/Users.js | 34 +++++++------- scm-ui/src/users/modules/users.js | 45 +++++++------------ scm-ui/src/users/types/User.js | 10 +++++ 11 files changed, 118 insertions(+), 91 deletions(-) create mode 100644 scm-ui/src/types/hal.js create mode 100644 scm-ui/src/users/types/User.js diff --git a/scm-ui/package.json b/scm-ui/package.json index 84776009b5..5c11dc9813 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -5,7 +5,6 @@ "private": true, "dependencies": { "classnames": "^2.2.5", - "flow-bin": "^0.75.0", "history": "^4.7.2", "react": "^16.4.1", "react-dom": "^16.4.1", @@ -34,6 +33,8 @@ "devDependencies": { "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", + "flow-bin": "^0.75.0", + "flow-typed": "^2.5.1", "prettier": "^1.13.7", "react-test-renderer": "^16.4.1" }, diff --git a/scm-ui/src/apiclient.js b/scm-ui/src/apiclient.js index 02d3263fec..0b945d1fbd 100644 --- a/scm-ui/src/apiclient.js +++ b/scm-ui/src/apiclient.js @@ -4,6 +4,7 @@ const apiUrl = process.env.API_URL || process.env.PUBLIC_URL || "/scm"; export const PAGE_NOT_FOUND_ERROR = Error("page not found"); +export const NOT_AUTHENTICATED_ERROR = Error("not authenticated"); const fetchOptions: RequestOptions = { credentials: "same-origin", @@ -15,7 +16,7 @@ const fetchOptions: RequestOptions = { function handleStatusCode(response: Response) { if (!response.ok) { if (response.status === 401) { - return response; + throw NOT_AUTHENTICATED_ERROR; } if (response.status === 404) { throw PAGE_NOT_FOUND_ERROR; @@ -26,11 +27,14 @@ function handleStatusCode(response: Response) { } function createUrl(url: string) { + if (url.indexOf("://") > 0) { + return url; + } return `${apiUrl}/api/rest/v2/${url}`; } class ApiClient { - get(url: string) { + get(url: string): Promise { return fetch(createUrl(url), fetchOptions).then(handleStatusCode); } @@ -38,7 +42,7 @@ class ApiClient { return this.httpRequestWithJSONBody(url, payload, "POST"); } - delete(url: string) { + delete(url: string): Promise { let options: RequestOptions = { method: "DELETE" }; @@ -46,7 +50,11 @@ class ApiClient { return fetch(createUrl(url), options).then(handleStatusCode); } - httpRequestWithJSONBody(url: string, payload: any, method: string) { + httpRequestWithJSONBody( + url: string, + payload: any, + method: string + ): Promise { let options: RequestOptions = { method: method, body: JSON.stringify(payload) diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index 1ee8df1c98..aa40fdc4a9 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -9,17 +9,19 @@ import { withRouter } from "react-router-dom"; type Props = { login: boolean, username: string, - getAuthState: any + getAuthState: () => void, + loading: boolean }; -class App extends Component { - componentWillMount() { +class App extends Component { + componentDidMount() { this.props.getAuthState(); } render() { - const { login, username } = this.props.login; - - if (!login) { + const { login, username, loading } = this.props; + if (loading) { + return
Loading...
; + } else if (!login) { return (
@@ -44,7 +46,7 @@ const mapDispatchToProps = dispatch => { }; const mapStateToProps = state => { - return { login: state.login }; + return state.login || {}; }; export default withRouter( diff --git a/scm-ui/src/modules/login.js b/scm-ui/src/modules/login.js index b4f505296d..c2bced3891 100644 --- a/scm-ui/src/modules/login.js +++ b/scm-ui/src/modules/login.js @@ -1,6 +1,6 @@ //@flow -import { apiClient } from "../apiclient"; +import { apiClient, NOT_AUTHENTICATED_ERROR } from "../apiclient"; const LOGIN_URL = "/auth/access_token"; const AUTHENTICATION_INFO_URL = "/me"; @@ -21,7 +21,7 @@ export function getIsAuthenticatedRequest() { } export function getIsAuthenticated() { - return function(dispatch: (any) => void) { + return function(dispatch: any => void) { dispatch(getIsAuthenticatedRequest()); return apiClient .get(AUTHENTICATION_INFO_URL) @@ -32,6 +32,13 @@ export function getIsAuthenticated() { if (data) { dispatch(isAuthenticated(data.username)); } + }) + .catch((error: Error) => { + if (error === NOT_AUTHENTICATED_ERROR) { + dispatch(isNotAuthenticated()); + } else { + // TODO: Handle errors other than not_authenticated + } }); }; } @@ -60,9 +67,9 @@ export function login(username: string, password: string) { cookie: true, grant_type: "password", username, - password, + password }; - return function(dispatch: (any) => void) { + return function(dispatch: any => void) { dispatch(loginRequest()); return apiClient.post(LOGIN_URL, login_data).then(response => { if (response.ok) { @@ -79,23 +86,29 @@ export function loginSuccessful() { }; } -export default function reducer(state: any = {}, action: any = {}) { +export default function reducer( + state: any = { loading: true }, + action: any = {} +) { switch (action.type) { case LOGIN: return { ...state, + loading: true, login: false, error: null }; case LOGIN_SUCCESSFUL: return { ...state, + loading: false, login: true, error: null }; case LOGIN_FAILED: return { ...state, + loading: false, login: false, error: action.payload }; @@ -103,12 +116,14 @@ export default function reducer(state: any = {}, action: any = {}) { return { ...state, login: true, + loading: false, username: action.username }; case IS_NOT_AUTHENTICATED: return { ...state, login: false, + loading: false, username: null, error: null }; diff --git a/scm-ui/src/types/hal.js b/scm-ui/src/types/hal.js new file mode 100644 index 0000000000..4c68bbab93 --- /dev/null +++ b/scm-ui/src/types/hal.js @@ -0,0 +1,6 @@ +// @flow +export type Link = { + href: string +}; + +export type Links = { [string]: Link }; diff --git a/scm-ui/src/users/containers/DeleteUserButton.js b/scm-ui/src/users/containers/DeleteUserButton.js index c26dc56fb6..15e14bc7f6 100644 --- a/scm-ui/src/users/containers/DeleteUserButton.js +++ b/scm-ui/src/users/containers/DeleteUserButton.js @@ -1,21 +1,17 @@ // @flow import React from "react"; +import type { User } from "../types/User"; type Props = { - user: any, + user: User, deleteUser: (link: string) => void }; class DeleteUser extends React.Component { - deleteUser = () => { this.props.deleteUser(this.props.user._links.delete.href); }; - if(deleteButtonClicked) { - let deleteButtonAsk =
You really want to remove this user?
- } - isDeletable = () => { return this.props.user._links.delete; }; @@ -28,7 +24,6 @@ class DeleteUser extends React.Component { - ); } } diff --git a/scm-ui/src/users/containers/DeleteUserButton.test.js b/scm-ui/src/users/containers/DeleteUserButton.test.js index efda995c86..4e1a1af663 100644 --- a/scm-ui/src/users/containers/DeleteUserButton.test.js +++ b/scm-ui/src/users/containers/DeleteUserButton.test.js @@ -1,42 +1,39 @@ -import React from 'react'; -import {configure, shallow} from 'enzyme'; +import React from "react"; +import { configure, shallow } from "enzyme"; import DeleteUserButton from "./DeleteUserButton"; -import Adapter from 'enzyme-adapter-react-16'; +import Adapter from "enzyme-adapter-react-16"; -import 'raf/polyfill'; +import "raf/polyfill"; configure({ adapter: new Adapter() }); -it('should render nothing, if the delete link is missing', () => { - +it("should render nothing, if the delete link is missing", () => { const user = { _links: {} }; - const button = shallow(); + const button = shallow(); expect(button.text()).toBe(""); }); -it('should render the button', () => { - +it("should render the button", () => { const user = { _links: { - "delete": { - "href": "/users" + delete: { + href: "/users" } } }; - const button = shallow(); + const button = shallow(); expect(button.text()).not.toBe(""); }); -it('should call the delete user function with delete url', () => { - +it("should call the delete user function with delete url", () => { const user = { _links: { - "delete": { - "href": "/users" + delete: { + href: "/users" } } }; @@ -47,7 +44,7 @@ it('should call the delete user function with delete url', () => { calledUrl = url; } - const button = shallow(); + const button = shallow(); button.simulate("click"); expect(calledUrl).toBe("/users"); diff --git a/scm-ui/src/users/containers/UserRow.js b/scm-ui/src/users/containers/UserRow.js index 5431178ba9..2f2dba001b 100644 --- a/scm-ui/src/users/containers/UserRow.js +++ b/scm-ui/src/users/containers/UserRow.js @@ -1,22 +1,26 @@ // @flow import React from "react"; import DeleteUserButton from "./DeleteUserButton"; +import type { User } from "../types/User"; type Props = { - user: any + user: User, + deleteUser: string => void }; export default class UserRow extends React.Component { render() { + const { user, deleteUser } = this.props; return ( - {this.props.user.displayName} - {this.props.user.mail} + {user.name} + {user.displayName} + {user.mail} - + - + ); diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 0702a56245..9c9914e6dd 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -2,27 +2,22 @@ import React from "react"; import { connect } from "react-redux"; -import { fetchUsersIfNeeded, fetchUsers } from "../modules/users"; +import { fetchUsers, deleteUser } from "../modules/users"; import Login from "../../containers/Login"; import UserRow from "./UserRow"; +import type { User } from "../types/User"; type Props = { login: boolean, - error: any, - users: any, - fetchUsersIfNeeded: () => void, + error: Error, + users: Array, fetchUsers: () => void, - fetchUsersIfNeeded: (url: string) => void, - + deleteUser: string => void }; class Users extends React.Component { - componentWillMount() { - this.props.fetchUsersIfNeeded(); - } - - componentDidUpdate() { - this.props.fetchUsersIfNeeded(); + componentDidMount() { + this.props.fetchUsers(); } render() { @@ -35,13 +30,20 @@ class Users extends React.Component { Name + Display Name E-Mail Admin {this.props.users.map((user, index) => { - return ; + return ( + + ); })} @@ -61,11 +63,11 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - fetchUsersIfNeeded: () => { - dispatch(fetchUsersIfNeeded()); - }, fetchUsers: () => { dispatch(fetchUsers()); + }, + deleteUser: (link: string) => { + dispatch(deleteUser(link)); } }; }; diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index 1fdc499535..14c4db1851 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -1,5 +1,6 @@ // @flow import { apiClient, PAGE_NOT_FOUND_ERROR } from "../../apiclient"; +import { ThunkDispatch } from "redux-thunk"; const FETCH_USERS = "scm/users/FETCH"; const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS"; @@ -34,7 +35,7 @@ function usersNotFound(url: string) { } export function fetchUsers() { - return function(dispatch: any => void) { + return function(dispatch: ThunkDispatch) { dispatch(requestUsers()); return apiClient .get(USERS_URL) @@ -66,21 +67,6 @@ function fetchUsersSuccess(users: any) { }; } -export function shouldFetchUsers(state: any): boolean { - if (state.users.users == null) { - return true; - } - return false; -} - -export function fetchUsersIfNeeded() { - return (dispatch, getState) => { - if (shouldFetchUsers(getState())) { - dispatch(fetchUsers()); - } - }; -} - function requestDeleteUser(url: string) { return { type: DELETE_USER, @@ -102,41 +88,42 @@ function deleteUserFailure(url: string, err: Error) { }; } -export function deleteUser(username: string) { - return function(dispatch) { - dispatch(requestDeleteUser(username)); +export function deleteUser(link: string) { + return function(dispatch: ThunkDispatch) { + dispatch(requestDeleteUser(link)); return apiClient - .delete(USERS_URL + "/" + username) + .delete(link) .then(() => { dispatch(deleteUserSuccess()); + dispatch(fetchUsers()); }) - .catch(err => dispatch(deleteUserFailure(username, err))); + .catch(err => dispatch(deleteUserFailure(link, err))); }; } export default function reducer(state: any = {}, action: any = {}) { switch (action.type) { case FETCH_USERS: + case DELETE_USER: return { ...state, - users: null + users: null, + loading: true }; case FETCH_USERS_SUCCESS: return { ...state, error: null, - users: action.payload._embedded.users + users: action.payload._embedded.users, + loading: false }; case FETCH_USERS_FAILURE: + case DELETE_USER_FAILURE: return { ...state, login: false, - error: action.payload - }; - case DELETE_USER_SUCCESS: - return { - ...state, - users: null + error: action.payload, + loading: false }; default: diff --git a/scm-ui/src/users/types/User.js b/scm-ui/src/users/types/User.js new file mode 100644 index 0000000000..9cf4e79728 --- /dev/null +++ b/scm-ui/src/users/types/User.js @@ -0,0 +1,10 @@ +//@flow +import type { Link, Links } from "../../types/hal"; + +export type User = { + displayName: string, + name: string, + mail: string, + admin: boolean, + _links: Links +}; From d35a56e07e9a071c50143d9c5f600303a4a04e2f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 11 Jul 2018 14:59:01 +0200 Subject: [PATCH 020/145] initial styling --- scm-ui/package.json | 11 +- scm-ui/public/favicon.ico | Bin 3870 -> 1150 bytes scm-ui/public/index.html | 2 +- scm-ui/public/manifest.json | 4 +- scm-ui/src/components/Header.js | 31 + scm-ui/src/components/InputField.js | 49 + scm-ui/src/components/Logo.js | 11 + scm-ui/src/components/PrimaryNavigation.js | 17 + .../src/components/PrimaryNavigationLink.js | 29 + scm-ui/src/components/SubmitButton.js | 38 + scm-ui/src/containers/App.css | 6435 +++++++++++++++++ scm-ui/src/containers/App.js | 35 +- scm-ui/src/containers/App.scss | 30 + scm-ui/src/containers/Login.js | 88 +- scm-ui/src/containers/Main.js | 26 +- scm-ui/src/containers/Navigation.js | 52 - scm-ui/src/images/blib.jpg | Bin 0 -> 20857 bytes scm-ui/src/images/logo.png | Bin 0 -> 15162 bytes 18 files changed, 6742 insertions(+), 116 deletions(-) create mode 100644 scm-ui/src/components/Header.js create mode 100644 scm-ui/src/components/InputField.js create mode 100644 scm-ui/src/components/Logo.js create mode 100644 scm-ui/src/components/PrimaryNavigation.js create mode 100644 scm-ui/src/components/PrimaryNavigationLink.js create mode 100644 scm-ui/src/components/SubmitButton.js create mode 100644 scm-ui/src/containers/App.css create mode 100644 scm-ui/src/containers/App.scss delete mode 100644 scm-ui/src/containers/Navigation.js create mode 100644 scm-ui/src/images/blib.jpg create mode 100644 scm-ui/src/images/logo.png diff --git a/scm-ui/package.json b/scm-ui/package.json index 5c11dc9813..c482349665 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -4,6 +4,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "bulma": "^0.7.1", "classnames": "^2.2.5", "history": "^4.7.2", "react": "^16.4.1", @@ -19,8 +20,12 @@ "redux-thunk": "^2.3.0" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", + "build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/", + "watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive", + "start-js": "react-scripts start", + "start": "npm-run-all -p watch-css start-js", + "build-js": "react-scripts build", + "build": "npm-run-all build-css build-js", "test": "jest", "eject": "react-scripts eject", "flow": "flow" @@ -35,6 +40,8 @@ "enzyme-adapter-react-16": "^1.1.1", "flow-bin": "^0.75.0", "flow-typed": "^2.5.1", + "node-sass-chokidar": "^1.3.0", + "npm-run-all": "^4.1.3", "prettier": "^1.13.7", "react-test-renderer": "^16.4.1" }, diff --git a/scm-ui/public/favicon.ico b/scm-ui/public/favicon.ico index a11777cc471a4344702741ab1c8a588998b1311a..e5803f340df8c825ba1344e189e192e34f983a9f 100644 GIT binary patch literal 1150 zcmcJNJ4?e*6vwY1D7X{^7l+0sX#^39F22Bg1N#x&baZzx;NY0i`oM@*P<$hbqBYf_ z;2>GF)!Le>^_33!01nsxX-#nuYX)!m_1xZbe&-(2Fjj|8Ai(e~u+e(PS{P&P07p2f zoFgz5x#p${^!vY7DLEMR+dGv?MSwtF$sCN9P6t%l`&sa}sUG*MPNLuP<5Der6O?Cn z5`FyrsN3+<1FivV5O5N`_S<0>f8Fo=bB`~dBkImckNJ8K;V*j~{CP()i<_cQYZiDk zKl%LOc*rQby9zIEN$%5n$Sth4Y4QR1&YB^pW zjsU#5$FQ$H6R+mCh!15P2(S&u?z4Hil8t7f{j9<0TCA`!Pz8ZlgLeh L1`y;UtsLM0i literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ diff --git a/scm-ui/public/index.html b/scm-ui/public/index.html index ed0ebafa1b..1891f54ac7 100644 --- a/scm-ui/public/index.html +++ b/scm-ui/public/index.html @@ -19,7 +19,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + SCM-Manager