Added optional authentication

This commit is contained in:
Henry Whitaker
2020-08-21 17:31:43 +01:00
parent cd87a902a7
commit e10b4dccde
31 changed files with 149588 additions and 107 deletions

View File

@@ -0,0 +1,86 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Container, Row, Col, Collapse, Button, Modal } from 'react-bootstrap';
import SessionsTable from './SessionsTable';
import ResetPassword from './ResetPassword';
export default class Authentication extends Component {
constructor(props) {
super(props)
this.state = {
showCollapse: false,
showModal: false
}
}
toggleCollapse = () => {
if(this.state.show) {
this.setState({
showCollapse: false
});
} else {
this.setState({
showCollapse: true
});
}
}
toggleModal = () => {
if(this.state.show) {
this.setState({
showModal: false
});
} else {
this.setState({
showModal: true
});
}
}
render() {
var showCollapse = this.state.showCollapse;
var showModal = this.state.showModal;
if( (window.config.auth == true && window.authenticated == true)) {
return (
<Container className="mb-4">
<Row>
<Col sm={{ span: 12 }} className="mb-3 text-center">
<div className="mouse" aria-controls="testsTable" onClick={this.toggleCollapse} aria-expanded={showCollapse}>
<h4 className="d-inline mr-2">Authentication</h4>
{(showCollapse) ?
<span className="ti-angle-up"></span>
:
<span className="ti-angle-down"></span>
}
</div>
</Col>
</Row>
<Collapse in={showCollapse}>
<div>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<ResetPassword />
</Col>
</Row>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<SessionsTable />
</Col>
</Row>
</div>
</Collapse>
</Container>
);
} else {
return (
<></>
);
}
}
}
if (document.getElementById('Authentication')) {
ReactDOM.render(<Authentication />, document.getElementById('Authentication'));
}

View File

@@ -0,0 +1,108 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Container, Row, Col, Collapse, Button, Modal, Form } from 'react-bootstrap';
import SessionsTable from './SessionsTable';
import Axios from 'axios';
import { toast } from 'react-toastify';
export default class ResetPassword extends Component {
constructor(props) {
super(props)
this.state = {
showModal: false,
currentPassword: '',
newPassword: '',
newPasswordConfirmation: '',
logoutDevices: false
}
}
toggleModal = () => {
if(this.state.showModal) {
this.setState({
showModal: false
});
} else {
this.setState({
showModal: true
});
}
}
updateTextField = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
updateCheckbox = (e) => {
this.setState({
[e.target.id]: e.target.checked
});
}
changePassword = (e) => {
e.preventDefault();
var data = {
currentPassword: this.state.currentPassword,
newPassword: this.state.newPassword,
newPassword_confirmation: this.state.newPasswordConfirmation,
logoutDevices: this.state.logoutDevices
}
var url = 'api/auth/change-password?token=' + window.token;
Axios.post(url, data)
.then((resp) => {
toast.success('Password updated');
this.toggleModal();
})
.catch((err) => {
if(err.response) {
for(var key in err.response.data.error) {
toast.error(err.response.data.error[key][0]);
}
}
})
}
render() {
var showModal = this.state.showModal;
return (
<div>
<Button variant="primary" onClick={this.toggleModal} className="mb-3">Change password</Button>
<Modal show={showModal} onHide={this.toggleModal}>
<Modal.Header closeButton>
<Modal.Title>Change password</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={this.changePassword}>
<Form.Group controlId="currentPassword">
<Form.Label>Current password</Form.Label>
<Form.Control type="password" onInput={this.updateTextField} required />
</Form.Group>
<Form.Group controlId="newPassword">
<Form.Label>New Password</Form.Label>
<Form.Control type="password" onInput={this.updateTextField} required />
</Form.Group>
<Form.Group controlId="newPasswordConfirmation">
<Form.Label>Confirm New Password</Form.Label>
<Form.Control type="password" onInput={this.updateTextField} required />
</Form.Group>
<Button variant="primary" type="submit" className="d-inline-block">Change password</Button>
<Form.Group controlId="logoutDevices" className="d-inline-block ml-2">
<Form.Check type="checkbox" label="Log everywhere out" onInput={this.updateCheckbox} />
</Form.Group>
</Form>
</Modal.Body>
</Modal>
</div>
);
}
}
if (document.getElementById('ResetPassword')) {
ReactDOM.render(<ResetPassword />, document.getElementById('ResetPassword'));
}

View File

@@ -0,0 +1,67 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Container, Row, Col, Table } from 'react-bootstrap';
import Axios from 'axios';
export default class SessionsTable extends Component {
constructor(props) {
super(props)
this.state = {
sessions: []
}
}
componentDidMount() {
this.getSessions();
}
getSessions = () => {
var url = 'api/auth/sessions?token=' + window.token;
Axios.get(url)
.then((resp) => {
this.setState({
sessions: resp.data.response
})
})
}
render() {
var sessions = this.state.sessions;
return (
<Container className="mb-4">
<Row>
<Col sm={{ span: 12 }} className="mb-3 text-center">
<h5>Login Sessions</h5>
<Table responsive>
<thead>
<tr>
<th>IP</th>
<th>Expires</th>
<th>Created at</th>
</tr>
</thead>
<tbody>
{sessions.map((e,i) => {
return(
<tr key={i}>
<td>{e.ip}</td>
<td>{new Date(e.expires * 1000).toLocaleDateString() + ' ' + new Date(e.expires * 1000).toLocaleTimeString()}</td>
<td>{e.created_at}</td>
</tr>
)
})}
</tbody>
</Table>
</Col>
</Row>
</Container>
);
}
}
if (document.getElementById('SessionsTable')) {
ReactDOM.render(<SessionsTable />, document.getElementById('SessionsTable'));
}

View File

@@ -6,7 +6,7 @@ import Axios from 'axios';
export default class Backup extends Component {
backup = (format) => {
var url = 'api/backup?format=' + format;
var url = 'api/backup?format=' + format + '&token=' + window.token;
toast.info('Your backup has started downloading...');

View File

@@ -16,11 +16,13 @@ export default class Changelog extends Component {
}
componentDidMount = () => {
this.getChangelog();
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
this.getChangelog();
}
}
getChangelog = () => {
Axios.get('api/update/changelog')
Axios.get('api/update/changelog?token=' + window.token)
.then((resp) => {
this.setState({
changelog: resp.data.data,

View File

@@ -8,21 +8,27 @@ import Restore from './Restore';
export default class DataRow extends Component {
render() {
return (
<Container className="mb-4">
<Row>
<Col sm={{ span: 12 }} className="text-center">
<p>Use these buttons to backup/restore your data</p>
</Col>
</Row>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<Backup />
<Restore />
</Col>
</Row>
</Container>
);
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
return (
<Container className="mb-4">
<Row>
<Col sm={{ span: 12 }} className="text-center">
<p>Use these buttons to backup/restore your data</p>
</Col>
</Row>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<Backup />
<Restore />
</Col>
</Row>
</Container>
);
} else {
return (
<></>
)
}
}
}

View File

@@ -148,7 +148,7 @@ export default class Restore extends Component {
uploadFile = () => {
var data = { data: this.state.data, format: this.state.format };
var url = 'api/restore';
var url = 'api/restore?token=' + window.token;
Axios.post(url, data)
.then((resp) => {

View File

@@ -45,7 +45,7 @@ export default class LatestResults extends Component {
}
newScan = () => {
var url = 'api/speedtest/run';
var url = 'api/speedtest/run?token=' + window.token;
Axios.get(url)
.then((resp) => {
@@ -78,25 +78,47 @@ export default class LatestResults extends Component {
</Container>
);
} else if(data === false) {
return (
<Container fluid>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<div>
<Button variant="primary" onClick={this.newScan}>Start your first test!</Button>
</div>
</Col>
</Row>
</Container>
);
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
return (
<Container fluid>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<div>
<Button variant="primary" onClick={this.newScan}>Start your first test!</Button>
</div>
</Col>
</Row>
</Container>
);
} else if(window.config.auth == true && window.authenticated == false) {
return (
<Container fluid>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<div>
<p>Please login to run the first test</p>
</div>
</Col>
</Row>
</Container>
);
}
} else {
return (
<Container fluid>
<Row>
<Col sm={{ span: 12 }} className="text-center mb-2">
<div>
<Button className="d-inline-block mx-3 mb-2" variant="primary" onClick={this.newScan}>Test again</Button>
<p className="text-muted mb-0 d-inline-block">Last test performed at: {new Date(data.data.created_at).toLocaleString()}</p>
{(window.config.auth == true && window.authenticated == true) || window.config.auth == false ?
<div>
<Button className="d-inline-block mx-3 mb-2" variant="primary" onClick={this.newScan}>Test again</Button>
<p className="text-muted mb-0 d-inline-block">Last test performed at: {new Date(data.data.created_at).toLocaleString()}</p>
</div>
:
<div>
<p className="text-muted mb-0 d-inline-block">Last test performed at: {new Date(data.data.created_at).toLocaleString()}</p>
</div>
}
</div>
</Col>
</Row>

View File

@@ -6,6 +6,8 @@ import Footer from './Footer';
import DataRow from '../Data/DataRow';
import TestsTable from '../Graphics/TestsTable';
import Settings from '../Settings/Settings';
import Login from '../Login';
import Authentication from '../Authentication/Authentication';
export default class HomePage extends Component {
@@ -13,10 +15,14 @@ export default class HomePage extends Component {
return (
<div>
<div className="my-4">
{(window.config.auth == true && window.authenticated == false) &&
<Login />
}
<LatestResults />
<HistoryGraph />
<TestsTable />
<Settings />
<Authentication />
<DataRow />
</div>
<Footer />

View File

@@ -1,10 +1,11 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Container, Row, Form, Toast } from 'react-bootstrap';
import { Container, Row, Form, Toast, Modal } from 'react-bootstrap';
import { Col } from 'react-bootstrap';
import { Button } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
import Cookies from 'js-cookie';
export default class Login extends Component {
constructor(props) {
@@ -12,59 +13,75 @@ export default class Login extends Component {
this.state = {
loginEmailInput: '',
loginPasswordInput: '',
toast: null,
loginPasswordInput: ''
}
}
updateTextField = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
login = (e) => {
e.preventDefault();
var data = {
email: this.state.loginEmailInput,
password: this.state.loginPasswordInput
};
var url = 'api/auth/login';
}
var url = 'api/auth/login';
Axios.post(url, data)
.then((resp) => {
var token = resp.data;
this.props.setToken(token);
var token = resp.data.access_token;
var expires = (resp.data.expires_in / 60) / 24;
Cookies.set('auth', token, { expires: expires })
window.location.reload(true);
})
.catch((err) => {
toast.error('Something went wrong. Please try again.')
});
}
updateTextField = (e) => {
this.setState({
[e.target.id]: e.target.value
});
toggleShow = () => {
if(this.state.show) {
this.setState({
show: false
})
} else {
this.setState({
show: true
})
}
}
render() {
var error = this.state.error;
var show = this.state.show;
return (
<Container>
<Row className="fullscreen align-items-center">
<Row>
<Col
lg={{ span: 4, offset: 4 }}
md={{ span: 6, offset: 3 }}
sm={{ span: 10, offset: 1 }}
xs={{ span: 12 }}
className="pb-2 text-center"
>
<Form onSubmit={this.login}>
<Form.Group controlId="loginEmailInput">
<Form.Label>Email address</Form.Label>
<Form.Control type="email" placeholder="admin@admin.com" onInput={this.updateTextField} required />
</Form.Group>
<Form.Group controlId="loginPasswordInput">
<Form.Label>Password</Form.Label>
<Form.Control type="password" onInput={this.updateTextField} required />
</Form.Group>
<Button variant="primary" type="submit">Login</Button>
</Form>
<Button variant="primary" onClick={this.toggleShow}>Login</Button>
<Modal show={show} onHide={this.toggleShow}>
<Modal.Header closeButton>
<Modal.Title>Login</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={this.login}>
<Form.Group controlId="loginEmailInput">
<Form.Label>Email address</Form.Label>
<Form.Control type="email" placeholder="admin@admin.com" onInput={this.updateTextField} required />
</Form.Group>
<Form.Group controlId="loginPasswordInput">
<Form.Label>Password</Form.Label>
<Form.Control type="password" onInput={this.updateTextField} required />
</Form.Group>
<Button variant="primary" type="submit">Login</Button>
</Form>
</Modal.Body>
</Modal>
</Col>
</Row>
</Container>

View File

@@ -19,7 +19,9 @@ export default class Settings extends Component {
}
componentDidMount = () => {
this.getData();
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
this.getData();
}
}
toggleShow = () => {
@@ -35,7 +37,7 @@ export default class Settings extends Component {
}
getData = () => {
var url = 'api/settings/';
var url = 'api/settings/?token=' + window.token;
Axios.get(url)
.then((resp) => {
@@ -53,6 +55,7 @@ export default class Settings extends Component {
buildSettingsCards = () => {
var e = this.state.data;
return (
<Row>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
@@ -215,39 +218,44 @@ export default class Settings extends Component {
if(!loading) {
var cards = this.buildSettingsCards();
}
return (
<div>
<Container className="my-4">
<Row>
<Col sm={{ span: 12 }} className="mb-3 text-center">
<div className="mouse" onClick={this.toggleShow}>
<h4 className="mb-0 mr-2 d-inline">Settings</h4>
{(show) ?
<span className="ti-angle-up"></span>
:
<span className="ti-angle-down"></span>
}
</div>
</Col>
</Row>
<Collapse in={show}>
<div>
<Row>
<Col sm={{ span: 12 }}>
{loading ?
<Loader small />
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
return (
<div>
<Container className="my-4">
<Row>
<Col sm={{ span: 12 }} className="mb-3 text-center">
<div className="mouse" onClick={this.toggleShow}>
<h4 className="mb-0 mr-2 d-inline">Settings</h4>
{(show) ?
<span className="ti-angle-up"></span>
:
cards
<span className="ti-angle-down"></span>
}
</Col>
</Row>
</div>
</Collapse>
</Container>
</div>
</Col>
</Row>
<Collapse in={show}>
<div>
<Row>
<Col sm={{ span: 12 }}>
{loading ?
<Loader small />
:
cards
}
</Col>
</Row>
</div>
</Collapse>
</Container>
</div>
);
</div>
);
} else {
return(
<></>
)
}
}
}

40
resources/js/index.js vendored
View File

@@ -4,10 +4,10 @@ import { BrowserRouter, Switch, Route, Redirect, useHistory } from 'react-router
import Axios from 'axios';
import ErrorPage from './components/ErrorPage';
import Loader from './components/Loader';
import Login from './components/Login';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import HomePage from './components/Home/HomePage';
import Cookies from 'js-cookie';
export default class Index extends Component {
constructor(props) {
@@ -15,7 +15,7 @@ export default class Index extends Component {
this.state = {
loading: true,
redirect: false
redirect: false,
}
}
@@ -29,10 +29,38 @@ export default class Index extends Component {
Axios.get(url)
.then((resp) => {
window.config = resp.data;
this.setState({
loading: false,
redirect: true,
});
if(window.config.auth === true) {
var authCookie = Cookies.get('auth');
if(authCookie == undefined) {
window.authenticated = false;
this.setState({
loading: false,
redirect: true,
});
} else {
var url = 'api/auth/me?token=' + authCookie;
Axios.get(url)
.then((resp) => {
window.authenticated = true;
window.token = authCookie;
})
.catch((err) => {
Cookies.remove('auth');
window.authenticated = false;
})
.finally(() => {
this.setState({
loading: false,
redirect: true,
});
})
}
} else {
this.setState({
loading: false,
redirect: true,
});
}
})
}