mirror of
https://git.rwth-aachen.de/acs/public/villas/web/
synced 2025-03-09 00:00:01 +01:00
Merge branch 'feature-external-auth' into 'master'
Feature external authentication See merge request acs/public/villas/web!75
This commit is contained in:
commit
7ae9679b5f
13 changed files with 350 additions and 25 deletions
|
@ -46,6 +46,11 @@ class App extends React.Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
AppDispatcher.dispatch({
|
||||
type: 'config/load',
|
||||
});
|
||||
|
||||
this.state = {
|
||||
showSidebarMenu: false,
|
||||
}
|
||||
|
|
|
@ -17,9 +17,73 @@
|
|||
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Container } from 'flux/utils';
|
||||
import LoginStore from '../user/login-store';
|
||||
import AppDispatcher from './app-dispatcher';
|
||||
|
||||
|
||||
class SidebarMenu extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
externalAuth: false,
|
||||
logoutLink: "",
|
||||
}
|
||||
}
|
||||
|
||||
static getStores() {
|
||||
return [LoginStore]
|
||||
}
|
||||
|
||||
static calculateState(prevState, props) {
|
||||
let config = LoginStore.getState().config;
|
||||
let logout_url = _.get(config, ['authentication', 'logout_url']);
|
||||
|
||||
if (logout_url) {
|
||||
return {
|
||||
externalAuth: true,
|
||||
logoutLink: logout_url,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
externalAuth: false,
|
||||
logoutLink: "/logout",
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'users/logout'
|
||||
});
|
||||
// The Login Store is deleted automatically
|
||||
|
||||
// discard login token and current User
|
||||
localStorage.setItem('token', '');
|
||||
localStorage.setItem('currentUser', '');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.externalAuth) {
|
||||
return (
|
||||
<div className="menu-sidebar">
|
||||
<h2>Menu</h2>
|
||||
<ul>
|
||||
<li><NavLink to="/home" activeClassName="active" title="Home">Home</NavLink></li>
|
||||
<li><NavLink to="/scenarios" activeClassName="active" title="Scenarios">Scenarios</NavLink></li>
|
||||
<li><NavLink to="/infrastructure" activeClassName="active" title="Infrastructure Components">Infrastructure Components</NavLink></li>
|
||||
{ this.props.currentRole === 'Admin' ?
|
||||
<li><NavLink to="/users" activeClassName="active" title="User Management">User Management</NavLink></li> : ''
|
||||
}
|
||||
<li><NavLink to="/account" title="Account">Account</NavLink></li>
|
||||
<a onClick={this.logout.bind(this)} href={this.state.logoutLink}>Logout</a>
|
||||
<li><NavLink to="/api" title="API Browser">API Browser</NavLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="menu-sidebar">
|
||||
<h2>Menu</h2>
|
||||
|
@ -32,7 +96,7 @@ class SidebarMenu extends React.Component {
|
|||
<li><NavLink to="/users" activeClassName="active" title="User Management">User Management</NavLink></li> : ''
|
||||
}
|
||||
<li><NavLink to="/account" title="Account">Account</NavLink></li>
|
||||
<li><NavLink to="/logout" title="Logout">Logout</NavLink></li>
|
||||
<li><NavLink to={this.state.logoutLink} title="Logout">Logout</NavLink></li>
|
||||
<li><NavLink to="/api" title="API Browser">API Browser</NavLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -40,4 +104,5 @@ class SidebarMenu extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default SidebarMenu;
|
||||
let fluxContainerConverter = require('../common/FluxContainerConverter');
|
||||
export default Container.create(fluxContainerConverter.convert(SidebarMenu));
|
43
src/config-reader.js
Normal file
43
src/config-reader.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* This file is part of VILLASweb.
|
||||
*
|
||||
* VILLASweb is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* VILLASweb is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
|
||||
******************************************************************************/
|
||||
|
||||
import RestDataManager from './common/data-managers/rest-data-manager';
|
||||
import RestAPI from './common/api/rest-api';
|
||||
import AppDispatcher from './common/app-dispatcher';
|
||||
|
||||
|
||||
class ConfigReader extends RestDataManager {
|
||||
constructor() {
|
||||
super('config', '/config');
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
RestAPI.get(this.makeURL('/config'), null).then(response => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'config/loaded',
|
||||
data: response,
|
||||
});
|
||||
}).catch(error => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'config/load-error',
|
||||
error: error,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default new ConfigReader();
|
|
@ -22,7 +22,11 @@ const config = {
|
|||
admin: {
|
||||
name: 'Institute for Automation of Complex Power Systems (ACS), RWTH Aachen University, Germany',
|
||||
mail: 'stvogel@eonerc.rwth-aachen.de'
|
||||
}
|
||||
},
|
||||
externalAuth: true,
|
||||
loginURL: '/oauth2/start?rd=/login/complete',
|
||||
provider: 'Jupyter',
|
||||
disableVillasLogin: false,
|
||||
};
|
||||
|
||||
export default config
|
||||
|
|
BIN
src/img/dog-waiting-bw.jpg
Normal file
BIN
src/img/dog-waiting-bw.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
|
@ -28,12 +28,15 @@ import Dashboard from './dashboard/dashboard'
|
|||
import InfrastructureComponents from './ic/ics';
|
||||
import Users from './user/users';
|
||||
import User from "./user/user";
|
||||
import LoginComplete from './user/login-complete'
|
||||
|
||||
|
||||
class Root extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path='/login/complete' component={LoginComplete} />
|
||||
<Route path='/login' component={Login} />
|
||||
<Route path='/logout' component={Logout} />
|
||||
<Route path='/' component={App} />
|
||||
|
|
|
@ -112,6 +112,13 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.verticalhorizontal {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 800px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menus
|
||||
*/
|
||||
|
@ -219,6 +226,26 @@ body {
|
|||
background-color: white;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login select
|
||||
*/
|
||||
.login-select {
|
||||
position: sticky;
|
||||
width: 300px;
|
||||
height: 150px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: 50px;
|
||||
margin-bottom: 100px;
|
||||
transform: translate(-50%);
|
||||
|
||||
|
||||
padding: 20px 20px;
|
||||
|
||||
background-color: #a8c7cf;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||
0 9px 18px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login form
|
||||
|
@ -234,6 +261,13 @@ body {
|
|||
0 9px 18px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border:0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tables
|
||||
*/
|
||||
|
|
107
src/user/login-complete.js
Normal file
107
src/user/login-complete.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* This file is part of VILLASweb.
|
||||
*
|
||||
* VILLASweb is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* VILLASweb is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
|
||||
******************************************************************************/
|
||||
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import AppDispatcher from '../common/app-dispatcher';
|
||||
import LoginStore from './login-store'
|
||||
import { Container } from 'flux/utils';
|
||||
|
||||
|
||||
class LoginComplete extends React.Component {
|
||||
constructor(props) {
|
||||
console.log("LoginComplete constructor");
|
||||
super(props);
|
||||
|
||||
AppDispatcher.dispatch({
|
||||
type: 'users/extlogin',
|
||||
});
|
||||
|
||||
this.state = {
|
||||
loginMessage: '',
|
||||
token: '',
|
||||
currentUser: '',
|
||||
secondsToWait: 99,
|
||||
}
|
||||
|
||||
this.timer = 0;
|
||||
this.startTimer = this.startTimer.bind(this);
|
||||
this.countDown = this.countDown.bind(this);
|
||||
this.stopTimer = this.stopTimer.bind(this);
|
||||
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.startTimer();
|
||||
this.setState({secondsToWait: 5});
|
||||
}
|
||||
|
||||
|
||||
static getStores(){
|
||||
return [LoginStore]
|
||||
}
|
||||
|
||||
static calculateState(prevState, props) {
|
||||
// We need to work with the login store here to trigger the re-render upon state change after login
|
||||
// Upon successful login, the token and currentUser are stored in the local storage as strings
|
||||
return {
|
||||
loginMessage: LoginStore.getState().loginMessage,
|
||||
token: LoginStore.getState().token,
|
||||
currentUser: LoginStore.getState().currentUser,
|
||||
}
|
||||
}
|
||||
|
||||
stopTimer() {
|
||||
console.log("stop timer");
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
if (this.timer == 0 && this.state.secondsToWait > 0) {
|
||||
// call function 'countDown' every 1000ms
|
||||
this.timer = setInterval(this.countDown, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
countDown() {
|
||||
let seconds = this.state.secondsToWait - 1;
|
||||
this.setState({secondsToWait: seconds});
|
||||
|
||||
// waiting time over, stop counting down
|
||||
if (seconds == 0) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.currentUser && this.state.currentUser !== "") {
|
||||
this.stopTimer();
|
||||
return (<Redirect to="/home" />);
|
||||
}
|
||||
else if (this.state.secondsToWait == 0) {
|
||||
this.stopTimer();
|
||||
return (<Redirect to="/login" />);
|
||||
} else {
|
||||
return (<div class="verticalhorizontal">
|
||||
<img style={{height: 300}}src={require('../img/dog-waiting-bw.jpg').default} alt="Waiting Dog" /></div>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fluxContainerConverter = require('../common/FluxContainerConverter');
|
||||
export default Container.create(fluxContainerConverter.convert(LoginComplete));
|
|
@ -19,6 +19,8 @@ import React, { Component } from 'react';
|
|||
import { Form, Button, FormGroup, FormControl, FormLabel, Col } from 'react-bootstrap';
|
||||
import RecoverPassword from './recover-password'
|
||||
import AppDispatcher from '../common/app-dispatcher';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
class LoginForm extends Component {
|
||||
constructor(props) {
|
||||
|
@ -56,17 +58,17 @@ class LoginForm extends Component {
|
|||
this.setState({ [event.target.id]: event.target.value, disableLogin });
|
||||
}
|
||||
|
||||
openRecoverPassword(){
|
||||
this.setState({forgottenPassword: true});
|
||||
openRecoverPassword() {
|
||||
this.setState({ forgottenPassword: true });
|
||||
}
|
||||
|
||||
closeRecoverPassword(){
|
||||
this.setState({forgottenPassword: false});
|
||||
closeRecoverPassword() {
|
||||
this.setState({ forgottenPassword: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
villaslogin() {
|
||||
return (
|
||||
<Form>
|
||||
<Form key="login_a">
|
||||
<FormGroup controlId="username">
|
||||
<FormLabel column={true}>Username</FormLabel>
|
||||
<Col>
|
||||
|
@ -89,7 +91,7 @@ class LoginForm extends Component {
|
|||
</div>
|
||||
}
|
||||
|
||||
<FormGroup style={{paddingTop: 15, paddingBottom: 5}}>
|
||||
<FormGroup style={{ paddingTop: 15, paddingBottom: 5 }}>
|
||||
<Col>
|
||||
<span className='solid-button'>
|
||||
<Button variant='secondary' style={{width: 90}} type="submit" disabled={this.state.disableLogin} onClick={e => this.login(e)}>Login</Button>
|
||||
|
@ -101,9 +103,28 @@ class LoginForm extends Component {
|
|||
<RecoverPassword show={this.state.forgottenPassword} onClose={() => this.closeRecoverPassword()} sessionToken={this.props.sessionToken} />
|
||||
|
||||
</Form>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let villasLogin = this.villaslogin();
|
||||
|
||||
if (this.props.config) {
|
||||
let externalLogin = _.get(this.props.config, ['authentication', 'external', 'enabled'])
|
||||
let provider = _.get(this.props.config, ['authentication', 'external', 'provider_name'])
|
||||
let url = _.get(this.props.config, ['authentication', 'external', 'authorize_url']) + "?rd=/login/complete"
|
||||
|
||||
if (externalLogin && provider && url) {
|
||||
return [
|
||||
villasLogin,
|
||||
<hr key="login_b"/>,
|
||||
<Button key="login_c" onClick={e => window.location = url } block>Sign in with {provider}</Button>
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return villasLogin;
|
||||
}
|
||||
}
|
||||
|
||||
export default LoginForm;
|
||||
|
|
|
@ -20,6 +20,7 @@ import { ReduceStore } from 'flux/utils';
|
|||
import AppDispatcher from '../common/app-dispatcher';
|
||||
import UsersDataManager from './users-data-manager';
|
||||
import ICDataDataManager from '../ic/ic-data-data-manager';
|
||||
import ConfigReader from '../config-reader';
|
||||
|
||||
class LoginStore extends ReduceStore {
|
||||
constructor() {
|
||||
|
@ -31,15 +32,30 @@ class LoginStore extends ReduceStore {
|
|||
currentUser: null,
|
||||
token: null,
|
||||
loginMessage: null,
|
||||
config: null,
|
||||
};
|
||||
}
|
||||
|
||||
reduce(state, action) {
|
||||
switch (action.type) {
|
||||
case 'config/load':
|
||||
ConfigReader.loadConfig();
|
||||
return state;
|
||||
|
||||
case 'config/loaded':
|
||||
return Object.assign({}, state, { config: action.data });
|
||||
|
||||
case 'config/load-error':
|
||||
return Object.assign({}, state, { config: null});
|
||||
|
||||
case 'users/login':
|
||||
UsersDataManager.login(action.username, action.password);
|
||||
return Object.assign({}, state, { loginMessage: null });
|
||||
|
||||
case 'users/extlogin':
|
||||
UsersDataManager.login();
|
||||
return Object.assign({}, state, { loginMessage: null });
|
||||
|
||||
case 'users/logout':
|
||||
// disconnect from all infrastructure components
|
||||
ICDataDataManager.closeAll();
|
||||
|
|
|
@ -26,10 +26,20 @@ import Header from '../common/header';
|
|||
import Footer from '../common/footer';
|
||||
import NotificationsDataManager from '../common/data-managers/notifications-data-manager';
|
||||
import LoginStore from './login-store'
|
||||
import AppDispatcher from '../common/app-dispatcher';
|
||||
|
||||
class Login extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
static getStores(){
|
||||
// load config in case the user goes directly to /login
|
||||
// otherwise it will be loaded in app constructor
|
||||
AppDispatcher.dispatch({
|
||||
type: 'config/load',
|
||||
});
|
||||
}
|
||||
|
||||
static getStores() {
|
||||
return [LoginStore]
|
||||
}
|
||||
|
||||
|
@ -40,6 +50,7 @@ class Login extends Component {
|
|||
loginMessage: LoginStore.getState().loginMessage,
|
||||
token: LoginStore.getState().token,
|
||||
currentUser: LoginStore.getState().currentUser,
|
||||
config: LoginStore.getState().config,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,7 +73,7 @@ class Login extends Component {
|
|||
<div className="login-container">
|
||||
<NavbarBrand>Login</NavbarBrand>
|
||||
|
||||
<LoginForm loginMessage={this.state.loginMessage} />
|
||||
<LoginForm loginMessage={this.state.loginMessage} config={this.state.config}/>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import AppDispatcher from '../common/app-dispatcher';
|
||||
|
||||
class Logout extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'users/logout'
|
||||
|
@ -40,4 +40,4 @@ class Logout extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default Logout;
|
||||
export default Logout;
|
|
@ -25,18 +25,34 @@ class UsersDataManager extends RestDataManager {
|
|||
}
|
||||
|
||||
login(username, password) {
|
||||
RestAPI.post(this.makeURL('/authenticate'), { username: username, password: password }).then(response => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'users/logged-in',
|
||||
token: response.token,
|
||||
currentUser: response.user,
|
||||
if (username && password) {
|
||||
RestAPI.post(this.makeURL('/authenticate/internal'), { username: username, password: password }).then(response => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'users/logged-in',
|
||||
token: response.token,
|
||||
currentUser: response.user,
|
||||
});
|
||||
}).catch(error => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'users/login-error',
|
||||
error: error
|
||||
});
|
||||
});
|
||||
}).catch(error => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'users/login-error',
|
||||
error: error
|
||||
} else { // external authentication
|
||||
RestAPI.post(this.makeURL('/authenticate/external'),).then(response => {
|
||||
console.log(response);
|
||||
AppDispatcher.dispatch({
|
||||
type: 'users/logged-in',
|
||||
token: response.token,
|
||||
currentUser: response.user,
|
||||
});
|
||||
}).catch(error => {
|
||||
AppDispatcher.dispatch({
|
||||
type: 'users/login-error',
|
||||
error: error
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue