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,59 @@
<?php
namespace App\Console\Commands;
use App\Helpers\SettingsHelper;
use Illuminate\Console\Command;
class AuthenticationCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'speedtest:auth {--enable} {--disable}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Toggle authentication for the app';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$opts = $this->options();
if($opts['enable'] === true && $opts['disable'] === true) {
$this->warn('Please specify only ONE of --enable and --disable');
} else if($opts['enable'] === false && $opts['disable'] === false) {
$this->warn('You need to specify either --enable OR --disable');
} else {
if($opts['enable'] === true) {
$this->info('Enabling authentication');
SettingsHelper::set('auth', true);
}
if($opts['disable'] === true) {
$this->info('Disabling authentication');
SettingsHelper::set('auth', false);
}
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use App\Auth\LoginSession;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Log;
class ClearOldSessionsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'speedtest:clear-sessions';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear expired sessions from database';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$now = Carbon::now()->timestamp;
$sessions = LoginSession::where('expires', '<=', $now)
->delete();
$this->info('Invalidated expired sessions');
}
}

View File

@@ -30,6 +30,7 @@ class Kernel extends ConsoleKernel
{ {
$schedule->job(new SpeedtestJob(true, config('integrations')))->cron(SettingsHelper::get('schedule')['value']); $schedule->job(new SpeedtestJob(true, config('integrations')))->cron(SettingsHelper::get('schedule')['value']);
$schedule->command('speedtest:overview')->cron('0 ' . SettingsHelper::get('speedtest_overview_time')->value . ' * * *'); $schedule->command('speedtest:overview')->cron('0 ' . SettingsHelper::get('speedtest_overview_time')->value . ' * * *');
$schedule->command('speedtest:clear-sessions')->everyMinute();
} }
/** /**

View File

@@ -145,7 +145,8 @@ class SettingsHelper {
'slack_webhook' => SettingsHelper::settingIsEditable('slack_webhook'), 'slack_webhook' => SettingsHelper::settingIsEditable('slack_webhook'),
'telegram_bot_token' => SettingsHelper::settingIsEditable('telegram_bot_token'), 'telegram_bot_token' => SettingsHelper::settingIsEditable('telegram_bot_token'),
'telegram_chat_id' => SettingsHelper::settingIsEditable('telegram_chat_id'), 'telegram_chat_id' => SettingsHelper::settingIsEditable('telegram_chat_id'),
] ],
'auth' => (bool)SettingsHelper::get('auth')->value
]; ];
} }

View File

@@ -227,7 +227,7 @@ class SpeedtestHelper {
$range = [ $range = [
Carbon::today() Carbon::today()
]; ];
for($i = 0; $i < $days; $i++) { for($i = 0; $i < ($days - 1); $i++) {
$prev = end($range); $prev = end($range);
$new = $prev->copy()->subDays(1); $new = $prev->copy()->subDays(1);
array_push($range, $new); array_push($range, $new);

View File

@@ -8,11 +8,14 @@ use App\Helpers\EmailVerificationHelper;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\LoginSession; use App\LoginSession;
use App\Rules\CurrentPasswordMatches;
use App\User; use App\User;
use DateTime; use DateTime;
use Hash;
use Illuminate\Support\Facades\Request as RequestFacade; use Illuminate\Support\Facades\Request as RequestFacade;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Log;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
class AuthController extends Controller class AuthController extends Controller
@@ -164,6 +167,10 @@ class AuthController extends Controller
[ 'expires', '>', time() ] [ 'expires', '>', time() ]
])->get(); ])->get();
$sessions = $sessions->map(function ($item) {
return collect($item)->forget(['token']);
});
return response()->json([ return response()->json([
'method' => 'get auth sessions', 'method' => 'get auth sessions',
'response' => $sessions 'response' => $sessions
@@ -211,4 +218,36 @@ class AuthController extends Controller
'success' => true, 'success' => true,
], 200); ], 200);
} }
public function changePassword(Request $request)
{
$rules = [
'currentPassword' => [ 'string', 'required', new CurrentPasswordMatches() ],
'newPassword' => [ 'required', 'string', 'confirmed', 'min:8' ],
'logoutDevices' => [ 'required', 'bool' ]
];
$validator = Validator::make($request->all(), $rules);
if($validator->fails()) {
return response()->json([
'method' => 'reset password',
'success' => false,
'error' => $validator->errors()
], 403);
}
$user = Auth::user();
$user->password = $request->newPassword;
$user->save();
if($request->logoutDevices == true) {
AuthLoginSession::where('user_id', $user->id)->update([ 'active' => false ]);
}
return response()->json([
'method' => 'reset password',
'success' => true
], 200);
}
} }

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Helpers\BackupHelper; use App\Helpers\BackupHelper;
use App\Helpers\SettingsHelper;
use DateTime; use DateTime;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@@ -12,6 +13,13 @@ use Illuminate\Http\JsonResponse;
class BackupController extends Controller class BackupController extends Controller
{ {
public function __construct()
{
if((bool)SettingsHelper::get('auth')->value === true) {
$this->middleware('auth:api');
}
}
/** /**
* Get backup of speedtests * Get backup of speedtests
* *

View File

@@ -13,6 +13,13 @@ use Ramsey\Uuid\Exception\InvalidUuidStringException;
class IntegrationsController extends Controller class IntegrationsController extends Controller
{ {
public function __construct()
{
if((bool)SettingsHelper::get('auth')->value === true) {
$this->middleware('auth:api');
}
}
/** /**
* Test the healthchecks config * Test the healthchecks config
* *

View File

@@ -13,6 +13,13 @@ use Illuminate\Support\Collection;
class SettingsController extends Controller class SettingsController extends Controller
{ {
public function __construct()
{
if((bool)SettingsHelper::get('auth')->value === true) {
$this->middleware('auth:api')
->except([ 'config' ]);
}
}
/** /**
* Return all settings * Return all settings

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Helpers\SettingsHelper;
use App\Helpers\SpeedtestHelper; use App\Helpers\SpeedtestHelper;
use App\Jobs\SpeedtestJob; use App\Jobs\SpeedtestJob;
use App\Speedtest; use App\Speedtest;
@@ -15,6 +16,13 @@ use Illuminate\Http\JsonResponse;
class SpeedtestController extends Controller class SpeedtestController extends Controller
{ {
public function __construct()
{
if((bool)SettingsHelper::get('auth')->value === true) {
$this->middleware('auth:api')
->only([ 'run', 'delete', 'deleteAll' ]);
}
}
/** /**
* Returns paginated list of speedtests * Returns paginated list of speedtests

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Helpers\SettingsHelper;
use Exception; use Exception;
use Updater; use Updater;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -9,6 +10,12 @@ use Illuminate\Http\JsonResponse;
class UpdateController extends Controller class UpdateController extends Controller
{ {
public function __construct()
{
if((bool)SettingsHelper::get('auth')->value === true) {
$this->middleware('auth:api');
}
}
/** /**
* Check for new update * Check for new update

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Rules;
use Hash;
use Illuminate\Contracts\Validation\Rule;
class CurrentPasswordMatches implements Rule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return Hash::check($value, auth()->user()->password);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The current password doesn\'t match.';
}
}

View File

@@ -0,0 +1,38 @@
<?php
use App\Helpers\SettingsHelper;
use App\Setting;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddAuthenticationSettings extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if(!SettingsHelper::get('auth')) {
Setting::create([
'name' => 'auth',
'value' => false,
'description' => 'Enable authentication.'
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Setting::whereIn('name', [
'auth',
])->delete();
}
}

5
package-lock.json generated
View File

@@ -6156,6 +6156,11 @@
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==", "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==",
"dev": true "dev": true
}, },
"js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@@ -28,6 +28,7 @@
"@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.10.4",
"chart.js": "^2.9.3", "chart.js": "^2.9.3",
"csv-file-validator": "^1.8.0", "csv-file-validator": "^1.8.0",
"js-cookie": "^2.2.1",
"react-bootstrap": "^1.3.0", "react-bootstrap": "^1.3.0",
"react-chartjs-2": "^2.10.0", "react-chartjs-2": "^2.10.0",
"react-router": "^5.2.0", "react-router": "^5.2.0",

10907
public/css/app.css vendored

File diff suppressed because one or more lines are too long

137935
public/js/app.js vendored

File diff suppressed because one or more lines are too long

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 { export default class Backup extends Component {
backup = (format) => { backup = (format) => {
var url = 'api/backup?format=' + format; var url = 'api/backup?format=' + format + '&token=' + window.token;
toast.info('Your backup has started downloading...'); toast.info('Your backup has started downloading...');

View File

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

View File

@@ -8,6 +8,7 @@ import Restore from './Restore';
export default class DataRow extends Component { export default class DataRow extends Component {
render() { render() {
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
return ( return (
<Container className="mb-4"> <Container className="mb-4">
<Row> <Row>
@@ -23,6 +24,11 @@ export default class DataRow extends Component {
</Row> </Row>
</Container> </Container>
); );
} else {
return (
<></>
)
}
} }
} }

View File

@@ -148,7 +148,7 @@ export default class Restore extends Component {
uploadFile = () => { uploadFile = () => {
var data = { data: this.state.data, format: this.state.format }; 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) Axios.post(url, data)
.then((resp) => { .then((resp) => {

View File

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

View File

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

View File

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

View File

@@ -19,8 +19,10 @@ export default class Settings extends Component {
} }
componentDidMount = () => { componentDidMount = () => {
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
this.getData(); this.getData();
} }
}
toggleShow = () => { toggleShow = () => {
if(this.state.show) { if(this.state.show) {
@@ -35,7 +37,7 @@ export default class Settings extends Component {
} }
getData = () => { getData = () => {
var url = 'api/settings/'; var url = 'api/settings/?token=' + window.token;
Axios.get(url) Axios.get(url)
.then((resp) => { .then((resp) => {
@@ -53,6 +55,7 @@ export default class Settings extends Component {
buildSettingsCards = () => { buildSettingsCards = () => {
var e = this.state.data; var e = this.state.data;
return ( return (
<Row> <Row>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}> <Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
@@ -215,7 +218,7 @@ export default class Settings extends Component {
if(!loading) { if(!loading) {
var cards = this.buildSettingsCards(); var cards = this.buildSettingsCards();
} }
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
return ( return (
<div> <div>
<Container className="my-4"> <Container className="my-4">
@@ -248,6 +251,11 @@ export default class Settings extends Component {
</div> </div>
); );
} else {
return(
<></>
)
}
} }
} }

32
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 Axios from 'axios';
import ErrorPage from './components/ErrorPage'; import ErrorPage from './components/ErrorPage';
import Loader from './components/Loader'; import Loader from './components/Loader';
import Login from './components/Login';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import HomePage from './components/Home/HomePage'; import HomePage from './components/Home/HomePage';
import Cookies from 'js-cookie';
export default class Index extends Component { export default class Index extends Component {
constructor(props) { constructor(props) {
@@ -15,7 +15,7 @@ export default class Index extends Component {
this.state = { this.state = {
loading: true, loading: true,
redirect: false redirect: false,
} }
} }
@@ -29,10 +29,38 @@ export default class Index extends Component {
Axios.get(url) Axios.get(url)
.then((resp) => { .then((resp) => {
window.config = resp.data; window.config = resp.data;
if(window.config.auth === true) {
var authCookie = Cookies.get('auth');
if(authCookie == undefined) {
window.authenticated = false;
this.setState({ this.setState({
loading: false, loading: false,
redirect: true, 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,
});
}
}) })
} }

View File

@@ -2,6 +2,7 @@
use App\Helpers\SpeedtestHelper; use App\Helpers\SpeedtestHelper;
use App\Http\Controllers\SpeedtestController; use App\Http\Controllers\SpeedtestController;
use App\Speedtest;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@@ -83,3 +84,29 @@ Route::group([
Route::post('/bulk', 'SettingsController@bulkStore') Route::post('/bulk', 'SettingsController@bulkStore')
->name('settings.bulk.update'); ->name('settings.bulk.update');
}); });
Route::group(
[
'middleware' => 'api',
'prefix' => 'auth'
],
function ($router) {
Route::post('register', 'AuthController@register')->name('auth.register');
Route::post('login', 'AuthController@login')->middleware('throttle:60,1')->name('auth.login');
Route::get('logout', 'AuthController@logout')->name('auth.logout');
Route::get('refresh', 'AuthController@refresh')->middleware('throttle:60,1')->name('auth.refresh');
Route::get('me', 'AuthController@me')->middleware('session_active')->name('auth.me');
Route::post('change-password', 'AuthController@changePassword')->middleware('session_active')->name('auth.change_password');
Route::group(
[
'middleware' => ['api', 'session_active'],
'prefix' => 'sessions'
],
function($router) {
Route::get('/', 'AuthController@getSessions')->name('auth.sessions.all');
Route::delete('/{id}', 'AuthController@deleteSession')->name('auth.sessions.delete');
}
);
}
);