1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/web/ synced 2025-03-09 00:00:01 +01:00

Add authentication

Uses jwt tokens, token is stored locally between refreshes
This commit is contained in:
Markus Grigull 2017-03-16 10:22:25 +01:00
parent 5a1de494fb
commit 5bf9ad9f8d
11 changed files with 374 additions and 14 deletions

View file

@ -11,9 +11,15 @@ import request from 'superagent/lib/client';
import Promise from 'es6-promise';
class RestAPI {
get(url) {
get(url, token) {
return new Promise(function (resolve, reject) {
request.get(url).set('Access-Control-Allow-Origin', '*').end(function (error, res) {
var req = request.get(url).set('Access-Control-Allow-Origin', '*');
if (token != null) {
req.set('x-access-token', token);
}
req.end(function (error, res) {
if (res == null || res.status !== 200) {
reject(error);
} else {
@ -23,9 +29,15 @@ class RestAPI {
});
}
post(url, body) {
post(url, body, token) {
return new Promise(function (resolve, reject) {
request.post(url).send(body).end(function (error, res) {
var req = request.post(url).send(body);
if (token != null) {
req.set('x-access-token', token);
}
req.end(function (error, res) {
if (res == null || res.status !== 200) {
reject(error);
} else {
@ -35,9 +47,15 @@ class RestAPI {
});
}
delete(url) {
delete(url, token) {
return new Promise(function (resolve, reject) {
request.delete(url).end(function (error, res) {
var req = request.delete(url);
if (token != null) {
req.set('x-access-token', token);
}
req.end(function (error, res) {
if (res == null || res.status !== 200) {
reject(error);
} else {
@ -47,9 +65,15 @@ class RestAPI {
});
}
put(url, body) {
put(url, body, token) {
return new Promise(function (resolve, reject) {
request.put(url).send(body).end(function (error, res) {
var req = request.put(url).send(body);
if (token != null) {
req.set('x-access-token', token);
}
req.end(function (error, res) {
if (res == null || res.status !== 200) {
reject(error);
} else {

View file

@ -12,7 +12,7 @@ import React, { Component } from 'react';
class Footer extends Component {
render() {
return (
<footer>
<footer className="app-footer">
Copyright &copy; {new Date().getFullYear()}
</footer>
);

View file

@ -0,0 +1,72 @@
/**
* File: login-form.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 15.03.2017
* Copyright: 2017, Institute for Automation of Complex Power Systems, EONERC
* This file is part of VILLASweb. All Rights Reserved. Proprietary and confidential.
* Unauthorized copying of this file, via any medium is strictly prohibited.
**********************************************************************************/
import React, { Component } from 'react';
import { Form, Button, FormGroup, FormControl, ControlLabel, Col } from 'react-bootstrap';
import AppDispatcher from '../app-dispatcher';
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: ''
}
}
login(event) {
// prevent from submitting the form since we send an action
event.preventDefault();
// send login action
AppDispatcher.dispatch({
type: 'users/login',
username: this.state.username,
password: this.state.password
});
}
handleChange(event) {
this.setState({ [event.target.id]: event.target.value });
}
render() {
return (
<Form horizontal>
<FormGroup controlId="username">
<Col componentClass={ControlLabel} sm={2}>
Username
</Col>
<Col sm={10}>
<FormControl type="text" placeholder="Username" onChange={(e) => this.handleChange(e)} />
</Col>
</FormGroup>
<FormGroup controlId="password">
<Col componentClass={ControlLabel} sm={2}>
Password
</Col>
<Col sm={10}>
<FormControl type="password" placeholder="Password" onChange={(e) => this.handleChange(e)} />
</Col>
</FormGroup>
<FormGroup>
<Col smOffset={2} sm={10}>
<Button type="submit" onClick={(e) => this.login(e)}>Login</Button>
</Col>
</FormGroup>
</Form>
);
}
}
export default LoginForm;

View file

@ -21,6 +21,7 @@ class SidebarMenu extends Component {
<li><Link to="/projects" activeClassName="active">Projects</Link></li>
<li><Link to="/simulations" activeClassName="active">Simulations</Link></li>
<li><Link to="/simulators" activeClassName="active">Simulators</Link></li>
<li><Link to="/logout">Logout</Link></li>
</ul>
</div>
);

View file

@ -15,6 +15,7 @@ import HTML5Backend from 'react-dnd-html5-backend';
import AppDispatcher from '../app-dispatcher';
import SimulationStore from '../stores/simulation-store';
import SimulatorStore from '../stores/simulator-store';
import UserStore from '../stores/user-store';
import Header from '../components/header';
import Footer from '../components/footer';
@ -24,17 +25,31 @@ import '../styles/app.css';
class App extends Component {
static getStores() {
return [ SimulationStore, SimulatorStore ];
return [ SimulationStore, SimulatorStore, UserStore ];
}
static calculateState() {
return {
simulators: SimulatorStore.getState(),
simulations: SimulationStore.getState()
simulations: SimulationStore.getState(),
currentUser: UserStore.getState().currentUser
};
}
componentWillMount() {
// if token stored locally, request user
const token = localStorage.getItem('token');
if (token != null && token !== '') {
AppDispatcher.dispatch({
type: 'users/logged-in',
token: token
});
} else {
// transition to login page
this.props.router.push('/login');
}
// load all simulators and simulations to fetch simulation data
AppDispatcher.dispatch({
type: 'simulators/start-load'

71
src/containers/login.js Normal file
View file

@ -0,0 +1,71 @@
/**
* File: login.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 15.03.2017
* Copyright: 2017, Institute for Automation of Complex Power Systems, EONERC
* This file is part of VILLASweb. All Rights Reserved. Proprietary and confidential.
* Unauthorized copying of this file, via any medium is strictly prohibited.
**********************************************************************************/
import React, { Component } from 'react';
import { Container } from 'flux/utils';
import { PageHeader } from 'react-bootstrap';
import LoginForm from '../components/login-form';
import Header from '../components/header';
import Footer from '../components/footer';
import AppDispatcher from '../app-dispatcher';
import UserStore from '../stores/user-store';
class Login extends Component {
static getStores() {
return [ UserStore ];
}
static calculateState() {
return {
currentUser: UserStore.getState().currentUser,
token: UserStore.getState().token
};
}
componentWillUpdate(nextProps, nextState) {
// if token stored locally, request user
const token = localStorage.getItem('token');
if (token != null && token !== '') {
AppDispatcher.dispatch({
type: 'users/logged-in',
token: token
});
}
// check if logged in
if (nextState.currentUser != null) {
// save login in local storage
localStorage.setItem('token', this.state.token);
// transition to index
this.props.router.push('/');
}
}
render() {
return (
<div>
<Header />
<div className="login-container">
<PageHeader>Login</PageHeader>
<LoginForm />
</div>
<Footer />
</div>
);
}
}
export default Container.create(Login);

51
src/containers/logout.js Normal file
View file

@ -0,0 +1,51 @@
/**
* File: logout.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 15.03.2017
* Copyright: 2017, Institute for Automation of Complex Power Systems, EONERC
* This file is part of VILLASweb. All Rights Reserved. Proprietary and confidential.
* Unauthorized copying of this file, via any medium is strictly prohibited.
**********************************************************************************/
import React, { Component } from 'react';
import { Container } from 'flux/utils';
import AppDispatcher from '../app-dispatcher';
import UserStore from '../stores/villas-store';
class Home extends Component {
static getStores() {
return [ UserStore ];
}
static calculateState() {
return {
currentUser: UserStore.getState().currentUser
};
}
componentWillMount() {
AppDispatcher.dispatch({
type: 'users/logout'
});
}
componentWillUpdate(nextProps, nextState) {
// check if logged out
if (nextState.currentUser == null) {
// discard login token
localStorage.setItem('token', '');
// transition to login page
this.props.router.push('/login');
}
}
render() {
return (
<span>Login out</span>
);
}
}
export default Container.create(Home);

View file

@ -0,0 +1,48 @@
/**
* File: users-data-manager.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 15.03.2017
* Copyright: 2017, Institute for Automation of Complex Power Systems, EONERC
* This file is part of VILLASweb. All Rights Reserved. Proprietary and confidential.
* Unauthorized copying of this file, via any medium is strictly prohibited.
**********************************************************************************/
import RestDataManager from './rest-data-manager';
import RestAPI from '../api/rest-api';
import AppDispatcher from '../app-dispatcher';
class UsersDataManager extends RestDataManager {
constructor() {
super('user', '/users');
}
login(username, password) {
RestAPI.post(this.makeURL('/authenticate'), { username: username, password: password }).then(response => {
AppDispatcher.dispatch({
type: 'users/logged-in',
token: response.token
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'users/login-error',
error: error
});
});
}
getCurrentUser(token) {
RestAPI.get(this.makeURL('/users/me'), token).then(response => {
AppDispatcher.dispatch({
type: 'users/current-user',
user: response
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'users/current-user-error',
error: error
});
});
}
}
export default new UsersDataManager();

View file

@ -18,6 +18,8 @@ import Simulators from './containers/simulators';
import Visualization from './containers/visualization';
import Simulations from './containers/simulations';
import Simulation from './containers/simulation';
import Login from './containers/login';
import Logout from './containers/logout';
class Root extends Component {
render() {
@ -35,7 +37,11 @@ class Root extends Component {
<Route path='/simulations' component={Simulations} />
<Route path='/simulations/:simulation' component={Simulation} />
<Route path='/logout' component={Logout} />
</Route>
<Route path='/login' component={Login} />
</Router>
);
}

58
src/stores/user-store.js Normal file
View file

@ -0,0 +1,58 @@
/**
* File: user-store.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 15.03.2017
* Copyright: 2017, Institute for Automation of Complex Power Systems, EONERC
* This file is part of VILLASweb. All Rights Reserved. Proprietary and confidential.
* Unauthorized copying of this file, via any medium is strictly prohibited.
**********************************************************************************/
import { ReduceStore } from 'flux/utils';
import AppDispatcher from '../app-dispatcher';
import UsersDataManager from '../data-managers/users-data-manager';
class UserStore extends ReduceStore {
constructor() {
super(AppDispatcher);
}
getInitialState() {
return {
users: [],
currentUser: null,
token: null
};
}
reduce(state, action) {
switch (action.type) {
case 'users/login':
UsersDataManager.login(action.username, action.password);
return state;
case 'users/logout':
// delete user and token
return { token: null, currentUser: null };
case 'users/logged-in':
// request logged-in user data
UsersDataManager.getCurrentUser(action.token);
return { token: action.token };
case 'users/current-user':
// save logged-in user
return { currentUser: action.user };
case 'users/login-error':
console.log(action);
return state;
default:
return state;
}
}
}
export default new UserStore();

View file

@ -22,7 +22,7 @@ body {
font: 16px 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.app header {
.app-header {
width: 100%;
height: 60px;
@ -32,7 +32,7 @@ body {
background-color: #fff;
}
.app header h1 {
.app-header h1 {
width: 100%;
margin: 0;
@ -40,7 +40,7 @@ body {
text-align: center;
}
.app footer {
.app-footer {
width: 100%;
margin-top: 20px;
@ -91,6 +91,20 @@ body {
list-style: none;
}
/**
* Login form
*/
.login-container {
width: 500px;
margin: 30px auto;
padding: 15px 20px;
background-color: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 9px 18px 0 rgba(0, 0, 0, 0.1);
}
/**
* Tables
*/